From 500256759efcb4399cc356f919307380e840349e Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 2 Aug 2024 12:39:57 +0300 Subject: [PATCH 01/62] [desktop] Minor tauri fixes and improvments --- Earthfile | 6 +- desktop/tauri/src-tauri/Cargo.lock | 201 ++++++++++++------ desktop/tauri/src-tauri/Cargo.toml | 4 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 1 - .../src-tauri/gen/schemas/linux-schema.json | 1 - desktop/tauri/src-tauri/src/cli.rs | 107 ++++++++++ desktop/tauri/src-tauri/src/main.rs | 78 +------ packaging/linux/portmaster.service | 8 +- 9 files changed, 267 insertions(+), 141 deletions(-) create mode 100644 desktop/tauri/src-tauri/src/cli.rs diff --git a/Earthfile b/Earthfile index 9d016b08..4a81900b 100644 --- a/Earthfile +++ b/Earthfile @@ -506,8 +506,8 @@ tauri-build: RUN echo output: $(ls -R "target/${target}/release") # Binaries - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/app" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app" - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/app.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app.exe" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app.exe" SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/WebView2Loader.dll" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/WebView2Loader.dll" # Installers @@ -575,7 +575,7 @@ tauri-build-windows-bundle: RUN echo "Version Suffix: $VERSION_SUFFIX" RUN echo output: $(ls -R "target/${target}/release") - RUN mv "target/${target}/release/app.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" + RUN mv "target/${target}/release/portmaster.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" RUN zip "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" -j portmaster-app${VERSION_SUFFIX}.exe "target/${target}/release/WebView2Loader.dll" -j WebView2Loader.dll SAVE ARTIFACT --if-exists "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 8d938985..8e037303 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -3768,7 +3768,7 @@ dependencies = [ name = "indexmap" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -4176,7 +4176,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "bytecount", - "clap 4.5.16", + "clap 4.5.9", "fancy-regex", "fraction", "getrandom 0.2.15", @@ -4645,8 +4645,8 @@ dependencies = [ ] [[package]] -name = "mio" -version = "1.0.2" +name = "muda" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ @@ -4980,14 +4980,35 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive 0.7.2", ] [[package]] name = "num_enum_derive" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", @@ -5145,9 +5166,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -5663,8 +5684,7 @@ dependencies = [ "nom", "num-bigint-dig", "num-traits", - "num_enum", - "ocb3", + "num_enum 0.7.2", "p256", "p384", "p521", @@ -5900,7 +5920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.4.0", + "indexmap 2.2.6", "quick-xml 0.32.0", "serde", "time", @@ -5962,6 +5982,58 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portmaster" +version = "0.1.0" +dependencies = [ + "assert_matches", + "cached", + "clap_lex", + "ctor", + "dark-light", + "dataurl", + "dirs 1.0.5", + "futures-util", + "gdk-pixbuf", + "gdk-pixbuf-sys", + "gio-sys", + "glib", + "glib-sys", + "gtk", + "gtk-sys", + "http 1.1.0", + "lazy_static", + "log", + "notify-rust", + "open", + "reqwest 0.12.5", + "rfd", + "rust-ini 0.20.0", + "serde", + "serde_json", + "sha", + "tauri", + "tauri-build", + "tauri-cli", + "tauri-plugin-clipboard-manager", + "tauri-plugin-dialog", + "tauri-plugin-log", + "tauri-plugin-notification", + "tauri-plugin-os", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-window-state", + "tauri-winrt-notification 0.3.1", + "thiserror", + "tokio", + "tokio-websockets", + "url", + "uuid", + "which", + "windows 0.54.0", + "windows-service", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5970,12 +6042,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precomputed-hash" @@ -7022,11 +7091,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "itoa 1.0.11", "memchr", "ryu", @@ -7085,7 +7154,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -7805,9 +7874,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.0-rc.8" +version = "2.0.0-beta.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8345ccc676ef16e26b61fc0f5340b4e770678b1e1f53f08c69ebdac5e56b422" +checksum = "3eab508aad4ae86e23865e294b20a7bb89bd7afea523897b7478329b841d4295" dependencies = [ "anyhow", "bytes", @@ -7877,9 +7946,9 @@ dependencies = [ [[package]] name = "tauri-bundler" -version = "2.0.1-rc.6" +version = "2.0.1-beta.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a0e120d67416e774923fc07a7b531ba6898ed08f0d6e07fe79d7e3da7e0c04" +checksum = "05cc597af0bcb88c1966ccba76a6dcde5b6530c97a4fc2d798f7d3fddf182944" dependencies = [ "anyhow", "ar", @@ -7921,15 +7990,15 @@ dependencies = [ [[package]] name = "tauri-cli" -version = "2.0.0-rc.8" +version = "2.0.0-beta.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc42d318b25224c40f88550308f8cc8746a38534d450ce9c553f8c93f335499" +checksum = "24129a8dc9c3c910fca89ba0fb87ad9028868abf9dadb2514cd569fef9ad53af" dependencies = [ "anyhow", "axum", "base64 0.22.1", "cargo-mobile2", - "clap 4.5.16", + "clap 4.5.9", "clap_complete", "colored", "common-path", @@ -8076,8 +8145,8 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils 2.0.0-rc.7", - "toml 0.8.19", + "tauri-utils 2.0.0-beta.19", + "toml 0.8.15", "walkdir", ] @@ -8099,9 +8168,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.0.0-rc.1" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf99017391fdc40b6c8ae0dae8d970cc8151a8177d48b8805f320f52cac0e3c" +checksum = "8860dd73c96969eb14813f9f04d8665f2853342670456fb6619d637137ef0d09" dependencies = [ "dunce", "log", @@ -8117,9 +8186,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.0.0-rc.0" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df6b25b1f2b7b61565e66c4dbee9eb39e5635d2a763206e380e07cc3f601a67" +checksum = "461853268fe115ca19ee21e5986d505944f0b826048fe1bd726d74753fdf1df6" dependencies = [ "anyhow", "glob", @@ -8136,9 +8205,9 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.0.0-rc.0" +version = "2.0.0-beta.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380d27f23c39cde6a73024e65d8ec9b5b0af861e968dbe16b3aad86cd2c578e5" +checksum = "80f80d78a6e8102acf05a1e735f006991a2abfc71566d4e484f820b7495cd52c" dependencies = [ "android_logger", "byte-unit", @@ -8158,9 +8227,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.0.0-rc.1" +version = "2.0.0-beta.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35677cdcdb4dc3f3ef6891f31b8ea314045064752912d66e676a4f1577b57ffa" +checksum = "a5a09eb4d9e0919ce954da68d3707ddb161e5e37447da26609d0d0f5aebbc69a" dependencies = [ "log", "notify-rust", @@ -8177,9 +8246,9 @@ dependencies = [ [[package]] name = "tauri-plugin-os" -version = "2.0.0-rc.0" +version = "2.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b54cfeb26356822d3be3db4282041b03552f573a694b6b28aded7d95c62a039" +checksum = "79a0466f11f45fd3f640a17b5ba5e34c62912c9b391141c818155125ae9f0917" dependencies = [ "gethostname", "log", @@ -8195,9 +8264,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.0-rc.1" +version = "2.0.0-beta.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2133e5c6fe2ae0263ff5920feed477d3b1413f89033f537966831b0cb6f61f8e" +checksum = "8a9fa8c4e3d9ec343f6c3eb081672045566128a6c48ff6f6eeea85251ff38d3f" dependencies = [ "encoding_rs", "log", @@ -8216,9 +8285,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.0.0-rc.0" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de552151b4c9ba9ff72c7244dccaadd47f88d1f0d5caa2603c5c1c12b7636edc" +checksum = "b21866e185e2f9c5d40afb851441e3292a4f94f4a26af6ae0dff6e7e5ba03f42" dependencies = [ "log", "serde", @@ -8231,9 +8300,9 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" -version = "2.0.0-rc.1" +version = "2.0.0-beta.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb6839228cbd225b95681c766cc51113e9dad62c4b3f6ebb102234413ba85ee2" +checksum = "b6f7cea222b8eeb3598c7b3e19e9c7e6b1c2d60207b87225e0c3bb1c24c8fdec" dependencies = [ "bitflags 2.6.0", "log", @@ -8246,9 +8315,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-rc.7" +version = "2.0.0-beta.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c72b844f387bfc3341c355f3e16b8cbf4161848fa4e348670effb222cd3ba5" +checksum = "fe978df03966febbebc608931dc2cf26ef94df70855a18b05f07134cf474de09" dependencies = [ "dpi", "gtk", @@ -8265,9 +8334,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-rc.7" +version = "2.0.0-beta.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73accf936a7cd01d1382de7850726fdf6c1f6ab3b01ccb7a0950cb852e332596" +checksum = "11e4d568f61095f507b3fc4254dfbfff3b20de2a1d66167ffca3f6d90b14db8f" dependencies = [ "cocoa 0.26.0", "gtk", @@ -8576,14 +8645,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", + "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", @@ -8711,7 +8781,7 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -8733,7 +8803,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -8746,7 +8816,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -8757,7 +8827,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -8768,7 +8838,7 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -9523,9 +9593,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "6.0.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" dependencies = [ "either", "home", @@ -10306,13 +10376,22 @@ dependencies = [ "zvariant 4.2.0", ] +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.6", +] + [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive", ] diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index db19639b..3dcfdef5 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "app" +name = "portmaster" version = "0.1.0" description = "Portmaster UI" authors = ["Safing"] license = "" repository = "" -default-run = "app" +default-run = "portmaster" edition = "2021" rust-version = "1.60" diff --git a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json index 233ccc01..85397e54 100644 --- a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json +++ b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"clipboard-manager":{"default_permission":null,"permissions":{"allow-read":{"identifier":"allow-read","description":"Enables the read command without any pre-configured scope.","commands":{"allow":["read"],"deny":[]}},"allow-write":{"identifier":"allow-write","description":"Enables the write command without any pre-configured scope.","commands":{"allow":["write"],"deny":[]}},"deny-read":{"identifier":"deny-read","description":"Denies the read command without any pre-configured scope.","commands":{"allow":[],"deny":["read"]}},"deny-write":{"identifier":"deny-write","description":"Denies the write command without any pre-configured scope.","commands":{"allow":[],"deny":["write"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} diff --git a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json index 797ccb5c..2119bf21 100644 --- a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json @@ -3072,4 +3072,3 @@ ] } } -} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json index 797ccb5c..2119bf21 100644 --- a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json @@ -3072,4 +3072,3 @@ ] } } -} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/cli.rs b/desktop/tauri/src-tauri/src/cli.rs new file mode 100644 index 00000000..d11c2aed --- /dev/null +++ b/desktop/tauri/src-tauri/src/cli.rs @@ -0,0 +1,107 @@ +use log::LevelFilter; + +#[cfg(not(debug_assertions))] +const DEFAULT_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Warn; + +#[cfg(debug_assertions)] +const DEFAULT_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Debug; + +#[derive(Debug)] +pub struct CliArguments { + // Path to the installation directory + pub data: Option, + + // Log level to use: off, error, warn, info, debug, trace + pub log_level: log::LevelFilter, + + // Start in the background without opening a window + pub background: bool, + + // Enable experimental notifications via Tauri. Replaces the notifier app. + pub with_prompts: bool, + + // Enable experimental prompt support via Tauri. Replaces the notifier app. + pub with_notifications: bool, +} + +impl CliArguments { + fn parse_log(&mut self, level: String) { + self.log_level = match level.as_ref() { + "off" => LevelFilter::Off, + "error" => LevelFilter::Error, + "warn" => LevelFilter::Warn, + "info" => LevelFilter::Info, + "debug" => LevelFilter::Debug, + "trace" => LevelFilter::Trace, + _ => DEFAULT_LOG_LEVEL, + } + } +} + +pub fn parse(raw: impl IntoIterator>) -> CliArguments { + let mut cli = CliArguments { + data: None, + log_level: DEFAULT_LOG_LEVEL, + background: false, + with_prompts: false, + with_notifications: false, + }; + + let raw = clap_lex::RawArgs::new(raw); + let mut cursor = raw.cursor(); + raw.next(&mut cursor); // Skip the bin + + while let Some(arg) = raw.next(&mut cursor) { + if let Some((long, value)) = arg.to_long() { + match long { + Ok("data") => { + if let Some(value) = value { + cli.data = Some(value.to_string_lossy().into_owned()); + } + } + Ok("log") => { + if let Some(value) = value { + cli.parse_log(value.to_string_lossy().into_owned()); + } + } + Ok("background") => { + cli.background = true; + } + Ok("with_prompts") => { + cli.with_prompts = true; + } + Ok("with_notifications") => { + cli.with_notifications = true; + } + _ => { + // Ignore unexpected flags + } + } + } else if let Some(mut shorts) = arg.to_short() { + while let Some(short) = shorts.next() { + match short { + Ok('l') => { + if let Some(value) = shorts.next_value_os() { + let mut str = value.to_string_lossy().into_owned(); + _ = str.remove(0); // remove first "=" from value (in -l=warn value will be "=warn") + cli.parse_log(str); + } + } + Ok('d') => { + if let Some(value) = shorts.next_value_os() { + let mut str = value.to_string_lossy().into_owned(); + _ = str.remove(0); // remove first "=" from value (in -d=/data value will be "=/data") + cli.data = Some(str); + } + } + Ok('b') => cli.background = true, + _ => { + // Ignore unexpected flags + } + } + } + } + } + + cli +} diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index d269c4f3..77c1d055 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -3,7 +3,6 @@ use std::{env, path::Path, time::Duration}; -use clap::{Arg, Command}; use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent}; // Library crates @@ -15,11 +14,12 @@ mod xdg; // App modules mod config; +mod cli; mod portmaster; mod traymenu; mod window; -use log::{debug, error, info, LevelFilter}; +use log::{debug, error, info}; use portmaster::PortmasterExt; use tauri_plugin_log::RotationStrategy; use traymenu::setup_tray_menu; @@ -30,12 +30,6 @@ extern crate lazy_static; const FALLBACK_TO_OLD_UI_EXIT_CODE: i32 = 77; -#[cfg(not(debug_assertions))] -const LOG_LEVEL: LevelFilter = LevelFilter::Warn; - -#[cfg(debug_assertions)] -const LOG_LEVEL: LevelFilter = LevelFilter::Debug; - #[derive(Clone, serde::Serialize)] struct Payload { args: Vec, @@ -49,23 +43,6 @@ struct WsHandler { is_first_connect: bool, } -struct CliArguments { - // Path to the installation directory - data: Option, - - // Log level to use: off, error, warn, info, debug, trace - log: String, - - // Start in the background without opening a window - background: bool, - - // Enable experimental notifications via Tauri. Replaces the notifier app. - with_prompts: bool, - - // Enable experimental prompt support via Tauri. Replaces the notifier app. - with_notifications: bool, -} - impl portmaster::Handler for WsHandler { fn name(&self) -> String { "main-handler".to_string() @@ -147,44 +124,7 @@ fn main() { std::process::exit(show_webview_not_installed_dialog()); } - let matches = Command::new("Portmaster") - .ignore_errors(true) - .arg( - Arg::new("data") - .short('d') - .long("data") - .required(false) - .help("Path to the installation directory."), - ) - .arg( - Arg::new("log") - .short('l') - .long("log") - .required(false) - .help("Log level to use: off, error, warn, info, debug, trace."), - ) - .arg( - Arg::new("background") - .short('b') - .long("background") - .required(false) - .help("Start in the background without opening a window."), - ) - .arg( - Arg::new("with_prompts") - .long("with_prompts") - .required(false) - .action(clap::ArgAction::SetTrue) - .help("Enable experimental notifications via Tauri. Replaces the notifier app."), - ) - .arg( - Arg::new("with_notifications") - .long("with_notifications") - .required(false) - .action(clap::ArgAction::SetTrue) - .help("Enable experimental prompt support via Tauri. Replaces the notifier app."), - ) - .get_matches(); + let cli_args = cli::parse(std::env::args()); let mut cli = CliArguments { data: None, @@ -245,7 +185,7 @@ fn main() { // Initialize Logging plugin. .plugin( tauri_plugin_log::Builder::default() - .level(log_level) + .level(cli_args.log_level) .rotation_strategy(RotationStrategy::KeepAll) .clear_targets() .target(log_target) @@ -287,16 +227,18 @@ fn main() { }); // Handle cli flags: - app.portmaster().set_show_after_bootstrap(!cli.background); app.portmaster() - .with_notification_support(cli.with_notifications); - app.portmaster().with_connection_prompts(cli.with_prompts); + .set_show_after_bootstrap(!cli_args.background); + app.portmaster() + .with_notification_support(cli_args.with_notifications); + app.portmaster() + .with_connection_prompts(cli_args.with_prompts); // prepare a custom portmaster plugin handler that will show the splash-screen // (if not in --background) and launch the tray-icon handler. let handler = WsHandler { handle: app.handle().clone(), - background: cli.background, + background: cli_args.background, is_first_connect: true, }; diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service index 7541d499..5490ac6f 100644 --- a/packaging/linux/portmaster.service +++ b/packaging/linux/portmaster.service @@ -30,12 +30,12 @@ ProtectKernelTunables=yes ProtectKernelLogs=yes ProtectControlGroups=yes PrivateDevices=yes -AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid -CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid +AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon +CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon StateDirectory=portmaster # TODO(ppacher): add --disable-software-updates once it's merged and the release process changed. -ExecStart=/usr/bin/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS -ExecStopPost=-/usr/bin/portmaster-start recover-iptables +ExecStart=/usr/bin/portmaster-core --data /opt/safing/portmaster -- $PORTMASTER_ARGS +ExecStopPost=-/usr/bin/portmaster-core recover-iptables [Install] WantedBy=multi-user.target From 9472a1f8f51b62553439081c06b1626680a977c5 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 2 Aug 2024 12:46:12 +0300 Subject: [PATCH 02/62] [desktop] Add update tauri process README --- desktop/tauri/src-tauri/README.md | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 desktop/tauri/src-tauri/README.md diff --git a/desktop/tauri/src-tauri/README.md b/desktop/tauri/src-tauri/README.md new file mode 100644 index 00000000..ad238810 --- /dev/null +++ b/desktop/tauri/src-tauri/README.md @@ -0,0 +1,39 @@ +# Update Tauri guide + +Check latest versions of tauri packages and update them accordingly: +```toml +[build-dependencies] +tauri-build = { version = "2.0.0-beta.19", features = [] } # Update to latest + +[dependencies] +# Tauri +tauri = { version = "2.0.0-beta.24", features = ["tray-icon", "image-png", "config-json5", "devtools"] } # Update to latest +tauri-plugin-shell = "2.0.0-beta" +tauri-plugin-dialog = "2.0.0-beta" +tauri-plugin-clipboard-manager = "2.0.0-beta" +tauri-plugin-os = "2.0.0-beta" +tauri-plugin-single-instance = "2.0.0-beta" +tauri-plugin-cli = "2.0.0-beta" +tauri-plugin-notification = "2.0.0-beta" +tauri-plugin-log = "2.0.0-beta" +tauri-plugin-window-state = "2.0.0-beta" + +tauri-cli = "2.0.0-beta.21" # Update to latest +``` + +> The plugins will be auto updated based on tauri version. + +Run: +```sh +cargo update +``` + +Update WIX installer template: +1. Get the latests [main.wxs](https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/main.wxs) template from the repository. +2. Replace the contents of `templates/main_original.wxs` with the repository version. +3. Replace the contents of `templates/main.wsx` and add the fallowing lines at the end of the file, inside the `Product` tag. +```xml + + + +``` From 4c340f7b709a9aabcc460917f450a1c823a83141 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 2 Aug 2024 17:50:15 +0300 Subject: [PATCH 03/62] [desktop] Fix all clippy warning. Add clippy to CI. --- .github/workflows/tauri.yml | 19 +++ Earthfile | 36 +++++ desktop/tauri/src-tauri/Cargo.toml | 5 +- desktop/tauri/src-tauri/src/main.rs | 44 +++--- .../tauri/src-tauri/src/portapi/message.rs | 143 +++++++++--------- .../src-tauri/src/portapi/models/config.rs | 6 +- .../src-tauri/src/portmaster/commands.rs | 6 +- desktop/tauri/src-tauri/src/portmaster/mod.rs | 17 +-- .../src-tauri/src/portmaster/notifications.rs | 8 +- .../tauri/src-tauri/src/service/systemd.rs | 6 +- desktop/tauri/src-tauri/src/traymenu.rs | 57 +++---- desktop/tauri/src-tauri/src/window.rs | 9 +- desktop/tauri/src-tauri/src/xdg/mod.rs | 19 +-- 13 files changed, 208 insertions(+), 167 deletions(-) diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml index 9ae0da99..a84f74e3 100644 --- a/.github/workflows/tauri.yml +++ b/.github/workflows/tauri.yml @@ -34,3 +34,22 @@ jobs: - name: Build tauri project run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-ci + + lint: + name: Linter + runs-on: ubuntu-latest + steps: + - uses: earthly/actions-setup@v1 + with: + version: v0.8.0 + - uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build tauri project + run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-lint diff --git a/Earthfile b/Earthfile index 4a81900b..a000f855 100644 --- a/Earthfile +++ b/Earthfile @@ -636,6 +636,42 @@ tauri-release: BUILD +tauri-build --target="${arch}" END +tauri-lint: + FROM +tauri-src + + # Clippy (rust linter) will try to build the project before it runs the linter. + # Make sure we have evrything needed to build the project. + ARG target="x86_64-unknown-linux-gnu" + + # if we want tauri to create the installer bundles we also need to provide all external binaries + # we need to do some magic here because tauri expects the binaries to include the rust target tripple. + # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING + # function from below to parse the triple and guess wich GOOS and GOARCH we need. + RUN mkdir /tmp/gobuild + RUN mkdir ./binaries + + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" + RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" + + # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling + # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. + # See documentation for externalBins for more information on how tauri searches for the binaries. + COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild + + # Place them in the correct folder with the rust target tripple attached. + FOR bin IN $(ls /tmp/gobuild) + # ${bin$.*} does not work in SET commands unfortunately so we use a shell + # snippet here: + RUN set -e ; \ + dest="./binaries/${bin}-${target}" ; \ + if [ -z "${bin##*.exe}" ]; then \ + dest="./binaries/${bin%.*}-${target}.exe" ; \ + fi ; \ + cp "/tmp/gobuild/${bin}" "${dest}" ; + END + DO rust+SET_CACHE_MOUNTS_ENV + RUN cargo clippy --all-targets --all-features -- -D warnings + kext-build: FROM ${rust_builder_image} diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index 3dcfdef5..f89ca02f 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -7,7 +7,7 @@ license = "" repository = "" default-run = "portmaster" edition = "2021" -rust-version = "1.60" +rust-version = "1.64" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -80,3 +80,6 @@ ctor = "0.2.6" # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # DO NOT REMOVE!! custom-protocol = [ "tauri/custom-protocol" ] + +[package.metadata.clippy] +allow = ["clippy::collapsible_else_if"] \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 77c1d055..c0adcb5c 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -48,7 +48,7 @@ impl portmaster::Handler for WsHandler { "main-handler".to_string() } - fn on_connect(&mut self, cli: portapi::client::PortAPI) -> () { + fn on_connect(&mut self, cli: portapi::client::PortAPI) { info!("connection established, creating main window"); // we successfully connected to Portmaster. Set is_first_connect to false @@ -116,11 +116,11 @@ fn show_webview_not_installed_dialog() -> i32 { } } - return FALLBACK_TO_OLD_UI_EXIT_CODE; + FALLBACK_TO_OLD_UI_EXIT_CODE } fn main() { - if let Err(_) = tauri::webview_version() { + if tauri::webview_version().is_err() { std::process::exit(show_webview_not_installed_dialog()); } @@ -251,8 +251,8 @@ fn main() { .build(tauri::generate_context!()) .expect("error while running tauri application"); - app.run(|handle, e| match e { - RunEvent::WindowEvent { label, event, .. } => { + app.run(|handle, e| { + if let RunEvent::WindowEvent { label, event, .. } = e { if label != "main" { // We only have one window at most so any other label is unexpected return; @@ -266,32 +266,22 @@ fn main() { // // Note: the above javascript does NOT trigger the CloseRequested event so // there's no need to handle that case here. - // - match event { - WindowEvent::CloseRequested { api, .. } => { - debug!( - "window (label={}) close request received, forwarding to user-interface.", - label - ); + if let WindowEvent::CloseRequested { api, .. } = event { + debug!( + "window (label={}) close request received, forwarding to user-interface.", + label + ); - api.prevent_close(); - if let Some(window) = handle.get_webview_window(label.as_str()) { - let result = window.emit("exit-requested", ""); - if let Err(err) = result { - error!("failed to emit event: {}", err.to_string()); - } - } else { - error!("window was None"); + api.prevent_close(); + if let Some(window) = handle.get_webview_window(label.as_str()) { + let result = window.emit("exit-requested", ""); + if let Err(err) = result { + error!("failed to emit event: {}", err.to_string()); } + } else { + error!("window was None"); } - _ => {} } } - - // TODO(vladimir): why was this needed? - // RunEvent::ExitRequested { api, .. } => { - // api.prevent_exit(); - // } - _ => {} }); } diff --git a/desktop/tauri/src-tauri/src/portapi/message.rs b/desktop/tauri/src-tauri/src/portapi/message.rs index 46eb7c77..6230bc55 100644 --- a/desktop/tauri/src-tauri/src/portapi/message.rs +++ b/desktop/tauri/src-tauri/src/portapi/message.rs @@ -26,48 +26,46 @@ pub enum MessageError { InvalidPayload(#[from] serde_json::Error), } - /// Payload defines the payload type and content of a PortAPI message. -/// +/// /// For the time being, only JSON payloads (indicated by a prefixed 'J' of the payload content) /// is directly supported in `Payload::parse()`. -/// +/// /// For other payload types (like CBOR, BSON, ...) it's the user responsibility to figure out /// appropriate decoding from the `Payload::UNKNOWN` variant. #[derive(PartialEq, Debug, Clone)] pub enum Payload { - JSON(String), - UNKNOWN(String), + Json(String), + Unknown(String), } /// ParseError is returned from `Payload::parse()`. #[derive(Debug, Error)] pub enum ParseError { #[error(transparent)] - JSON(#[from] serde_json::Error), + Json(#[from] serde_json::Error), #[error("unknown error while parsing")] - UNKNOWN + Unknown, } - impl Payload { /// Parse the payload into T. - /// + /// /// Only JSON parsing is supported for now. See [Payload] for more information. - pub fn parse<'a, T>(self: &'a Self) -> std::result::Result + pub fn parse<'a, T>(&'a self) -> std::result::Result where - T: serde::de::Deserialize<'a> { - + T: serde::de::Deserialize<'a>, + { match self { - Payload::JSON(blob) => Ok(serde_json::from_str::(blob.as_str())?), - Payload::UNKNOWN(_) => Err(ParseError::UNKNOWN), + Payload::Json(blob) => Ok(serde_json::from_str::(blob.as_str())?), + Payload::Unknown(_) => Err(ParseError::Unknown), } } } /// Supports creating a Payload instance from a String. -/// +/// /// See [Payload] for more information. impl std::convert::From for Payload { fn from(value: String) -> Payload { @@ -77,10 +75,10 @@ impl std::convert::From for Payload { match first { Some(c) => match c { - 'J' => Payload::JSON(rest), - _ => Payload::UNKNOWN(value), + 'J' => Payload::Json(rest), + _ => Payload::Unknown(value), }, - None => Payload::UNKNOWN("".to_string()) + None => Payload::Unknown("".to_string()), } } } @@ -89,10 +87,10 @@ impl std::convert::From for Payload { impl std::fmt::Display for Payload { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Payload::JSON(payload) => { + Payload::Json(payload) => { write!(f, "J{}", payload) - }, - Payload::UNKNOWN(payload) => { + } + Payload::Unknown(payload) => { write!(f, "{}", payload) } } @@ -100,9 +98,9 @@ impl std::fmt::Display for Payload { } /// Message is an internal representation of a PortAPI message. -/// Users should more likely use `portapi::types::Request` and `portapi::types::Response` +/// Users should more likely use `portapi::types::Request` and `portapi::types::Response` /// instead of directly using `Message`. -/// +/// /// The struct is still public since it might be useful for debugging or to implement new /// commands not yet supported by the `portapi::types` crate. #[derive(PartialEq, Debug, Clone)] @@ -115,23 +113,23 @@ pub struct Message { /// Implementation to marshal a PortAPI message into it's wire-format representation /// (which is a string). -/// +/// /// Note that this conversion does not check for invalid messages! impl std::convert::From for String { fn from(value: Message) -> Self { - let mut result = "".to_owned(); + let mut result = String::new(); result.push_str(value.id.to_string().as_str()); - result.push_str("|"); + result.push('|'); result.push_str(&value.cmd); if let Some(key) = value.key { - result.push_str("|"); + result.push('|'); result.push_str(key.as_str()); } if let Some(payload) = value.payload { - result.push_str("|"); + result.push('|'); result.push_str(payload.to_string().as_str()) } @@ -141,15 +139,15 @@ impl std::convert::From for String { /// An implementation for `String::parse()` to convert a wire-format representation /// of a PortAPI message to a Message instance. -/// +/// /// Any errors returned from `String::parse()` will be of type `MessageError` impl std::str::FromStr for Message { type Err = MessageError; fn from_str(line: &str) -> Result { - let parts = line.split("|").collect::>(); + let parts = line.split('|').collect::>(); - let id = match parts.get(0) { + let id = match parts.first() { Some(s) => match (*s).parse::() { Ok(id) => Ok(id), Err(_) => Err(MessageError::InvalidID), @@ -163,18 +161,15 @@ impl std::str::FromStr for Message { }? .to_string(); - let key = parts.get(2) - .and_then(|key| Some(key.to_string())); + let key = parts.get(2).map(|key| key.to_string()); + let payload: Option = parts.get(3).map(|p| p.to_string().into()); - let payload : Option = parts.get(3) - .and_then(|p| Some(p.to_string().into())); - - return Ok(Message { + Ok(Message { id, cmd, key, - payload: payload - }); + payload, + }) } } @@ -191,67 +186,79 @@ mod tests { #[test] fn payload_to_string() { - let p = Payload::JSON("{}".to_string()); + let p = Payload::Json("{}".to_string()); assert_eq!(p.to_string(), "J{}"); - let p = Payload::UNKNOWN("some unknown content".to_string()); + let p = Payload::Unknown("some unknown content".to_string()); assert_eq!(p.to_string(), "some unknown content"); } #[test] fn payload_from_string() { let p: Payload = "J{}".to_string().into(); - assert_eq!(p, Payload::JSON("{}".to_string())); + assert_eq!(p, Payload::Json("{}".to_string())); let p: Payload = "some unknown content".to_string().into(); - assert_eq!(p, Payload::UNKNOWN("some unknown content".to_string())); + assert_eq!(p, Payload::Unknown("some unknown content".to_string())); } #[test] fn payload_parse() { let p: Payload = "J{\"a\": 100, \"s\": \"string\"}".to_string().into(); - let t: Test = p.parse() - .expect("Expected payload parsing to work"); + let t: Test = p.parse().expect("Expected payload parsing to work"); - assert_eq!(t, Test{ - a: 100, - s: "string".to_string(), - }); + assert_eq!( + t, + Test { + a: 100, + s: "string".to_string(), + } + ); } #[test] fn parse_message() { - let m = "10|insert|some:key|J{}".parse::() + let m = "10|insert|some:key|J{}" + .parse::() .expect("Expected message to parse"); - assert_eq!(m, Message{ - id: 10, - cmd: "insert".to_string(), - key: Some("some:key".to_string()), - payload: Some(Payload::JSON("{}".to_string())), - }); + assert_eq!( + m, + Message { + id: 10, + cmd: "insert".to_string(), + key: Some("some:key".to_string()), + payload: Some(Payload::Json("{}".to_string())), + } + ); - let m = "1|done".parse::() + let m = "1|done" + .parse::() .expect("Expected message to parse"); - assert_eq!(m, Message{ - id: 1, - cmd: "done".to_string(), - key: None, - payload: None - }); + assert_eq!( + m, + Message { + id: 1, + cmd: "done".to_string(), + key: None, + payload: None + } + ); - let m = "".parse::() - .expect_err("Expected parsing to fail"); - if let MessageError::InvalidID = m {} else { + let m = "".parse::().expect_err("Expected parsing to fail"); + if let MessageError::InvalidID = m { + } else { panic!("unexpected error value: {}", m) } - let m = "1".parse::() + let m = "1" + .parse::() .expect_err("Expected parsing to fail"); - if let MessageError::MissingCommand = m {} else { + if let MessageError::MissingCommand = m { + } else { panic!("unexpected error value: {}", m) } } diff --git a/desktop/tauri/src-tauri/src/portapi/models/config.rs b/desktop/tauri/src-tauri/src/portapi/models/config.rs index e29474a8..72b8a1d8 100644 --- a/desktop/tauri/src-tauri/src/portapi/models/config.rs +++ b/desktop/tauri/src-tauri/src/portapi/models/config.rs @@ -1,5 +1,5 @@ -use serde::*; use super::super::message::Payload; +use serde::*; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct BooleanValue { @@ -13,6 +13,6 @@ impl TryInto for BooleanValue { fn try_into(self) -> Result { let str = serde_json::to_string(&self)?; - Ok(Payload::JSON(str)) + Ok(Payload::Json(str)) } -} \ No newline at end of file +} diff --git a/desktop/tauri/src-tauri/src/portmaster/commands.rs b/desktop/tauri/src-tauri/src/portmaster/commands.rs index d8e9db08..39c05788 100644 --- a/desktop/tauri/src-tauri/src/portmaster/commands.rs +++ b/desktop/tauri/src-tauri/src/portmaster/commands.rs @@ -86,7 +86,7 @@ pub fn get_app_info( matching_path, }; - if id == "" { + if id.is_empty() { id = uuid::Uuid::new_v4().to_string() } let cloned = id.clone(); @@ -137,7 +137,7 @@ pub fn get_app_info( pub fn get_service_manager_status(window: Window, response_id: String) -> Result { let mut id = response_id; - if id == "" { + if id.is_empty() { id = uuid::Uuid::new_v4().to_string(); } let cloned = id.clone(); @@ -161,7 +161,7 @@ pub fn get_service_manager_status(window: Window, response_id: St pub fn start_service(window: Window, response_id: String) -> Result { let mut id = response_id; - if id == "" { + if id.is_empty() { id = uuid::Uuid::new_v4().to_string(); } let cloned = id.clone(); diff --git a/desktop/tauri/src-tauri/src/portmaster/mod.rs b/desktop/tauri/src-tauri/src/portmaster/mod.rs index 80049860..0941f306 100644 --- a/desktop/tauri/src-tauri/src/portmaster/mod.rs +++ b/desktop/tauri/src-tauri/src/portmaster/mod.rs @@ -32,14 +32,13 @@ use std::{ }; use log::{debug, error}; -use serde; use std::sync::Mutex; use tauri::{AppHandle, Emitter, Manager, Runtime}; -const PORTMASTER_BASE_URL: &'static str = "http://127.0.0.1:817/api/v1/"; +const PORTMASTER_BASE_URL: &str = "http://127.0.0.1:817/api/v1/"; pub trait Handler { - fn on_connect(&mut self, cli: PortAPI) -> (); + fn on_connect(&mut self, cli: PortAPI); fn on_disconnect(&mut self); fn name(&self) -> String; } @@ -81,10 +80,7 @@ impl PortmasterInterface { let map = self.state.lock(); if let Ok(map) = map { - match map.get(&key) { - Some(value) => Some(value.clone()), - None => None, - } + map.get(&key).cloned() } else { None } @@ -129,11 +125,8 @@ impl PortmasterInterface { /// Returns the current portapi client. pub fn get_api(&self) -> Option { - if let Ok(mut api) = self.api.lock() { - match &mut *api { - Some(api) => Some(api.clone()), - None => None, - } + if let Ok(api) = self.api.lock() { + (*api).clone() } else { None } diff --git a/desktop/tauri/src-tauri/src/portmaster/notifications.rs b/desktop/tauri/src-tauri/src/portmaster/notifications.rs index bf851230..452db996 100644 --- a/desktop/tauri/src-tauri/src/portmaster/notifications.rs +++ b/desktop/tauri/src-tauri/src/portmaster/notifications.rs @@ -29,13 +29,13 @@ pub async fn notification_handler(cli: PortAPI) { } // Skip if this action has already been acted on - if n.selected_action_id != "" { + if n.selected_action_id.is_empty() { return; } show_notification(&cli, key, n).await; } Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse notification: {}", err); } _ => { @@ -81,7 +81,7 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) { let _ = cli_clone .request(Request::Update( key, - Payload::JSON( + Payload::Json( json!({ "SelectedActionID": value }) @@ -125,7 +125,7 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) { let _ = cli .request(Request::Update( key, - Payload::JSON( + Payload::Json( json!({ "SelectedActionID": value }) diff --git a/desktop/tauri/src-tauri/src/service/systemd.rs b/desktop/tauri/src-tauri/src/service/systemd.rs index 770a9139..c316f375 100644 --- a/desktop/tauri/src-tauri/src/service/systemd.rs +++ b/desktop/tauri/src-tauri/src/service/systemd.rs @@ -26,7 +26,7 @@ impl From for ServiceManagerError { .ok() .filter(|s| !s.trim().is_empty()) }) - .unwrap_or_else(|| format!("Failed to run `systemctl`")); + .unwrap_or_else(|| "Failed to run `systemctl`".to_string()); ServiceManagerError::Other(output.status, msg) } @@ -231,11 +231,11 @@ fn trim_newline(s: &mut String) { } fn get_sudo_cmd() -> std::result::Result { - if let Ok(_) = fs::metadata("/usr/bin/pkexec") { + if fs::metadata("/usr/bin/pkexec").is_ok() { return Ok(SudoCommand::Pkexec); } - if let Ok(_) = fs::metadata("/usr/bin/gksudo") { + if fs::metadata("/usr/bin/gksudo").is_ok() { return Ok(SudoCommand::Gksu); } diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 9d775475..c947bb9e 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -46,13 +46,11 @@ enum IconColor { static CURRENT_ICON_COLOR: RwLock = RwLock::new(IconColor::Red); pub static USER_THEME: RwLock = RwLock::new(dark_light::Mode::Default); -lazy_static! { - static ref SPN_STATUS: Mutex>> = Mutex::new(None); - static ref SPN_BUTTON: Mutex>> = Mutex::new(None); - static ref GLOBAL_STATUS: Mutex>> = Mutex::new(None); -} +static SPN_STATUS: Mutex>> = Mutex::new(None); +static SPN_BUTTON: Mutex>> = Mutex::new(None); +static GLOBAL_STATUS: Mutex>> = Mutex::new(None); -const PM_TRAY_ICON_ID: &'static str = "pm_icon"; +const PM_TRAY_ICON_ID: &str = "pm_icon"; // Icons @@ -64,9 +62,9 @@ fn get_theme_mode() -> dark_light::Mode { } fn get_green_icon() -> &'static [u8] { - const LIGHT_GREEN_ICON: &'static [u8] = + const LIGHT_GREEN_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_green_64.png"); - const DARK_GREEN_ICON: &'static [u8] = + const DARK_GREEN_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_green_64.png"); match get_theme_mode() { @@ -76,9 +74,9 @@ fn get_green_icon() -> &'static [u8] { } fn get_blue_icon() -> &'static [u8] { - const LIGHT_BLUE_ICON: &'static [u8] = + const LIGHT_BLUE_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_blue_64.png"); - const DARK_BLUE_ICON: &'static [u8] = + const DARK_BLUE_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_blue_64.png"); match get_theme_mode() { dark_light::Mode::Light => DARK_BLUE_ICON, @@ -87,20 +85,27 @@ fn get_blue_icon() -> &'static [u8] { } fn get_red_icon() -> &'static [u8] { - const LIGHT_RED_ICON: &'static [u8] = + const LIGHT_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_red_64.png"); const DARK_RED_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); match get_theme_mode() { + const DARK_RED_ICON: &'static [u8] = + include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); + let mode = dark_light::detect(); + match mode { + const DARK_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); + let mode = dark_light::detect(); + match mode { dark_light::Mode::Light => DARK_RED_ICON, _ => LIGHT_RED_ICON, } } fn get_yellow_icon() -> &'static [u8] { - const LIGHT_YELLOW_ICON: &'static [u8] = + const LIGHT_YELLOW_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_yellow_64.png"); - const DARK_YELLOW_ICON: &'static [u8] = + const DARK_YELLOW_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_yellow_64.png"); match get_theme_mode() { dark_light::Mode::Light => DARK_YELLOW_ICON, @@ -257,10 +262,8 @@ pub fn setup_tray_menu( button_state, } = event { - if let MouseButton::Left = button { - if let MouseButtonState::Down = button_state { - let _ = open_window(tray.app_handle()); - } + if let (MouseButton::Left, MouseButtonState::Down) = (button, button_state) { + let _ = open_window(tray.app_handle()); } } }) @@ -269,20 +272,20 @@ pub fn setup_tray_menu( } pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { - // iterate over the subsytems and check if there's a module failure - let failure = subsystems - .values() - .into_iter() - .map(|s| &s.module_status) - .fold((subsystem::FAILURE_NONE, "".to_string()), |mut acc, s| { + // iterate over the subsystems and check if there's a module failure + let failure = subsystems.values().map(|s| &s.module_status).fold( + (subsystem::FAILURE_NONE, "".to_string()), + |mut acc, s| { for m in s { if m.failure_status > acc.0 { acc = (m.failure_status, m.failure_msg.clone()) } } acc - }); + }, + ); + #[allow(clippy::collapsible_else_if)] if failure.0 == subsystem::FAILURE_NONE { if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) { _ = global_status.set_text("Status: Secured"); @@ -405,7 +408,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse subsystem: {}", err); } _ => { @@ -437,7 +440,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse spn status value: {}", err) }, _ => { @@ -466,7 +469,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_spn_ui_state(value.value.unwrap_or(false)); }, Err(err) => match err { - ParseError::JSON(err) => { + ParseError::Json(err) => { error!("failed to parse config value: {}", err) }, _ => { diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index b5e526a4..6cdcfe08 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -6,9 +6,8 @@ use tauri::{ use crate::{portmaster::PortmasterExt, traymenu}; -const LIGHT_PM_ICON: &'static [u8] = - include_bytes!("../../../../assets/data/icons/pm_light_512.png"); -const DARK_PM_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png"); +const LIGHT_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_512.png"); +const DARK_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png"); /// Either returns the existing "main" window or creates a new one. /// @@ -54,7 +53,7 @@ pub fn create_main_window(app: &AppHandle) -> Result { set_window_icon(&window); #[cfg(debug_assertions)] - if let Ok(_) = std::env::var("TAURI_SHOW_IMMEDIATELY") { + if std::env::var("TAURI_SHOW_IMMEDIATELY").is_ok() { debug!("[tauri] TAURI_SHOW_IMMEDIATELY is set, opening window"); if let Err(err) = window.show() { @@ -92,7 +91,7 @@ pub fn close_splash_window(app: &AppHandle) -> Result<()> { let _ = window.hide(); return window.destroy(); } - return Err(tauri::Error::WindowNotFound); + Err(tauri::Error::WindowNotFound) } pub fn hide_splash_window(app: &AppHandle) -> Result<()> { diff --git a/desktop/tauri/src-tauri/src/xdg/mod.rs b/desktop/tauri/src-tauri/src/xdg/mod.rs index 8ff6fd11..3db67942 100644 --- a/desktop/tauri/src-tauri/src/xdg/mod.rs +++ b/desktop/tauri/src-tauri/src/xdg/mod.rs @@ -18,7 +18,6 @@ use std::{ }; use thiserror::Error; -use dirs; use ini::{Ini, ParseOption}; static mut GTK_DEFAULT_THEME: Option<*mut GtkIconTheme> = None; @@ -146,7 +145,7 @@ pub fn get_app_info(process_info: ProcessInfo) -> Result { .unwrap() .insert(process_info.exec_path, None); - Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into()) + Err(Error::new(ErrorKind::NotFound, "failed to find app info".to_string()).into()) } else { // sort matches by length matches.sort_by(|a, b| a.1.cmp(&b.1)); @@ -178,7 +177,7 @@ pub fn get_app_info(process_info: ProcessInfo) -> Result { }; } - Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into()) + Err(Error::new(ErrorKind::NotFound, "failed to find app info".to_string()).into()) } } @@ -336,7 +335,7 @@ fn try_get_app_info( } } - if result.len() > 0 { + if !result.is_empty() { Ok(result) } else { Err(Error::new(ErrorKind::NotFound, "no matching .desktop files found").into()) @@ -393,7 +392,7 @@ fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> { // - network // name_without_ext - .split("-") + .split('-') .for_each(|part| icons.push(part)); for name in icons { @@ -554,15 +553,7 @@ mod tests { matching_path: bin.clone(), pid: 0, }) - .expect( - format!( - "expected to find app info for {} ({})", - bin, - cmd.to_string() - ) - .as_str(), - ); - + .unwrap_or_else(|_| panic!("expected to find app info for {} ({})", bin, cmd)); let empty_string = String::from(""); // just make sure all fields are populated From 556e5dd92168a8b8af4e9bed0d07c5b668b8d445 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 5 Aug 2024 14:58:35 +0300 Subject: [PATCH 04/62] [desktop] tauri remove some global variables. --- desktop/tauri/src-tauri/src/traymenu.rs | 88 ++++++++++++------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index c947bb9e..600af46d 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -4,11 +4,12 @@ use std::sync::{Mutex, RwLock}; use std::{collections::HashMap, sync::atomic::Ordering}; use log::{debug, error}; +use tauri::menu::{Menu, MenuItemKind}; use tauri::tray::{MouseButton, MouseButtonState}; -use tauri::Manager; +use tauri::Runtime; use tauri::{ image::Image, - menu::{MenuBuilder, MenuItem, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, + menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, tray::{TrayIcon, TrayIconBuilder}, Wry, }; @@ -46,9 +47,9 @@ enum IconColor { static CURRENT_ICON_COLOR: RwLock = RwLock::new(IconColor::Red); pub static USER_THEME: RwLock = RwLock::new(dark_light::Mode::Default); -static SPN_STATUS: Mutex>> = Mutex::new(None); -static SPN_BUTTON: Mutex>> = Mutex::new(None); -static GLOBAL_STATUS: Mutex>> = Mutex::new(None); +static SPN_STATUS_KEY: &str = "spn_status"; +static SPN_BUTTON_KEY: &str = "spn_toggle"; +static GLOBAL_STATUS_KEY: &str = "global_status"; const PM_TRAY_ICON_ID: &str = "pm_icon"; @@ -135,13 +136,9 @@ pub fn setup_tray_menu( .enabled(false) .build(app) .unwrap(); - { - let mut button_ref = GLOBAL_STATUS.lock()?; - *button_ref = Some(global_status.clone()); - } // Setup SPN status - let spn_status = MenuItemBuilder::with_id("spn_status", "SPN: Disabled") + let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, "SPN: Disabled") .enabled(false) .build(app) .unwrap(); @@ -149,15 +146,10 @@ pub fn setup_tray_menu( let mut button_ref = SPN_STATUS.lock()?; *button_ref = Some(spn_status.clone()); } - // Setup SPN button - let spn = MenuItemBuilder::with_id("spn_toggle", "Enable SPN") + let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, "Enable SPN") .build(app) .unwrap(); - { - let mut button_ref = SPN_BUTTON.lock()?; - *button_ref = Some(spn.clone()); - } let system_theme = MenuItemBuilder::with_id("system_theme", "System") .build(app) @@ -185,7 +177,7 @@ pub fn setup_tray_menu( &global_status, &PredefinedMenuItem::separator(app)?, &spn_status, - &spn, + &spn_button, &PredefinedMenuItem::separator(app)?, &theme_menu, &PredefinedMenuItem::separator(app)?, @@ -271,7 +263,12 @@ pub fn setup_tray_menu( Ok(icon) } -pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { +pub fn update_icon( + icon: AppIcon, + menu: Option>, + subsystems: HashMap, + spn_status: String, +) { // iterate over the subsystems and check if there's a module failure let failure = subsystems.values().map(|s| &s.module_status).fold( (subsystem::FAILURE_NONE, "".to_string()), @@ -285,14 +282,13 @@ pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_st }, ); - #[allow(clippy::collapsible_else_if)] - if failure.0 == subsystem::FAILURE_NONE { - if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) { - _ = global_status.set_text("Status: Secured"); - } - } else { - if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) { - _ = global_status.set_text(format!("Status: {}", failure.1)); + if let Some(menu) = menu { + if let Some(MenuItemKind::MenuItem(global_status)) = menu.get(GLOBAL_STATUS_KEY) { + if failure.0 == subsystem::FAILURE_NONE { + _ = global_status.set_text("Status: Secured"); + } else { + _ = global_status.set_text(format!("Status: {}", failure.1)); + } } } @@ -405,7 +401,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { Ok(n) => { subsystems.insert(n.id.clone(), n); - update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { @@ -437,7 +433,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { debug!("SPN status update: {}", value.status); spn_status = value.status.clone(); - update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { @@ -466,7 +462,9 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { if let Some((_, payload)) = res { match payload.parse::() { Ok(value) => { - update_spn_ui_state(value.value.unwrap_or(false)); + if let Some(menu) = app.menu() { + update_spn_ui_state(menu, value.value.unwrap_or(false)); + } }, Err(err) => match err { ParseError::Json(err) => { @@ -563,23 +561,23 @@ fn save_theme(app: &tauri::AppHandle, mode: dark_light::Mode) { } Err(err) => error!("failed to load config file: {}", err), } + if let Some(menu) = app.menu() { + update_spn_ui_state(menu, false); + } + _ = icon.set_icon(Some(Image::from_bytes(get_red_icon()).unwrap())); } -fn update_spn_ui_state(enabled: bool) { - let mut spn_status = SPN_STATUS.lock().unwrap(); - let Some(spn_status_ref) = &mut *spn_status else { - return; - }; - let mut spn_btn = SPN_BUTTON.lock().unwrap(); - let Some(spn_btn_ref) = &mut *spn_btn else { - return; - }; - if enabled { - _ = spn_status_ref.set_text("SPN: Connected"); - _ = spn_btn_ref.set_text("Disable SPN"); - } else { - _ = spn_status_ref.set_text("SPN: Disabled"); - _ = spn_btn_ref.set_text("Enable SPN"); +fn update_spn_ui_state(menu: Menu, enabled: bool) { + if let (Some(MenuItemKind::MenuItem(spn_status)), Some(MenuItemKind::MenuItem(spn_btn))) = + (menu.get(SPN_STATUS_KEY), menu.get(SPN_BUTTON_KEY)) + { + if enabled { + _ = spn_status.set_text("SPN: Connected"); + _ = spn_btn.set_text("Disable SPN"); + } else { + _ = spn_status.set_text("SPN: Disabled"); + _ = spn_btn.set_text("Enable SPN"); + } + SPN_STATE.store(enabled, Ordering::Release); } - SPN_STATE.store(enabled, Ordering::SeqCst); } From abf444630b25289baf28f3eedd041ffcecf4553c Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 13 Aug 2024 18:13:34 +0300 Subject: [PATCH 05/62] [WIP] New updater --- Earthfile | 7 +- base/updater/get.go | 95 ++++--- desktop/tauri/src-tauri/tauri.conf.json5 | 1 + service/updates/bundle.go | 247 +++++++++++++++++ service/updates/index.go | 110 ++++++++ service/updates/main.go | 323 ++++++++++------------- service/updates/module.go | 117 +++++++- service/updates/registry.go | 1 + service/updates/upgrader.go | 12 +- 9 files changed, 663 insertions(+), 250 deletions(-) create mode 100644 service/updates/bundle.go create mode 100644 service/updates/index.go create mode 100644 service/updates/registry.go diff --git a/Earthfile b/Earthfile index a000f855..c2c43497 100644 --- a/Earthfile +++ b/Earthfile @@ -462,17 +462,14 @@ tauri-build: # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. # See documentation for externalBins for more information on how tauri searches for the binaries. - COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild + COPY (+go-build/output --CMDS="portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild # Place them in the correct folder with the rust target tripple attached. FOR bin IN $(ls /tmp/gobuild) # ${bin$.*} does not work in SET commands unfortunately so we use a shell # snippet here: RUN set -e ; \ - dest="./binaries/${bin}-${target}" ; \ - if [ -z "${bin##*.exe}" ]; then \ - dest="./binaries/${bin%.*}-${target}.exe" ; \ - fi ; \ + dest="./binaries/${bin}" ; \ cp "/tmp/gobuild/${bin}" "${dest}" ; END diff --git a/base/updater/get.go b/base/updater/get.go index d50d28b3..7b54c8e6 100644 --- a/base/updater/get.go +++ b/base/updater/get.go @@ -1,12 +1,8 @@ package updater import ( - "context" "errors" "fmt" - "net/http" - - "github.com/safing/portmaster/base/log" ) // Errors returned by the updater package. @@ -19,59 +15,60 @@ var ( // GetFile returns the selected (mostly newest) file with the given // identifier or an error, if it fails. func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { - reg.RLock() - res, ok := reg.resources[identifier] - reg.RUnlock() - if !ok { - return nil, ErrNotFound - } + return nil, fmt.Errorf("invalid file: %s", identifier) + // reg.RLock() + // res, ok := reg.resources[identifier] + // reg.RUnlock() + // if !ok { + // return nil, ErrNotFound + // } - file := res.GetFile() - // check if file is available locally - if file.version.Available { - file.markActiveWithLocking() + // file := res.GetFile() + // // check if file is available locally + // if file.version.Available { + // file.markActiveWithLocking() - // Verify file, if configured. - _, err := file.Verify() - if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { - // TODO: If verification is required, try deleting the resource and downloading it again. - return nil, fmt.Errorf("failed to verify file: %w", err) - } + // // Verify file, if configured. + // _, err := file.Verify() + // if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { + // // TODO: If verification is required, try deleting the resource and downloading it again. + // return nil, fmt.Errorf("failed to verify file: %w", err) + // } - return file, nil - } + // return file, nil + // } - // check if online - if !reg.Online { - return nil, ErrNotAvailableLocally - } + // // check if online + // if !reg.Online { + // return nil, ErrNotAvailableLocally + // } - // check download dir - err := reg.tmpDir.Ensure() - if err != nil { - return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) - } + // // check download dir + // err := reg.tmpDir.Ensure() + // if err != nil { + // return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) + // } - // Start registry operation. - reg.state.StartOperation(StateFetching) - defer reg.state.EndOperation() + // // Start registry operation. + // reg.state.StartOperation(StateFetching) + // defer reg.state.EndOperation() - // download file - log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) - client := &http.Client{} - for tries := range 5 { - err = reg.fetchFile(context.TODO(), client, file.version, tries) - if err != nil { - log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) - } else { - file.markActiveWithLocking() + // // download file + // log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) + // client := &http.Client{} + // for tries := range 5 { + // err = reg.fetchFile(context.TODO(), client, file.version, tries) + // if err != nil { + // log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) + // } else { + // file.markActiveWithLocking() - // TODO: We just download the file - should we verify it again? - return file, nil - } - } - log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) - return nil, err + // // TODO: We just download the file - should we verify it again? + // return file, nil + // } + // } + // log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) + // return nil, err } // GetVersion returns the selected version of the given identifier. diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index 7363808c..beba5cd7 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -73,6 +73,7 @@ "release": "1", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", diff --git a/service/updates/bundle.go b/service/updates/bundle.go new file mode 100644 index 00000000..5214db1b --- /dev/null +++ b/service/updates/bundle.go @@ -0,0 +1,247 @@ +package updates + +import ( + "archive/zip" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/safing/portmaster/base/log" +) + +const MaxUnpackSize = 1 << 30 // 2^30 == 1GB + +type Artifact struct { + Filename string `json:"Filename"` + SHA256 string `json:"SHA256"` + URLs []string `json:"URLs"` + Platform string `json:"Platform,omitempty"` + Unpack string `json:"Unpack,omitempty"` + Version string `json:"Version,omitempty"` +} + +type Bundle struct { + Name string `json:"Bundle"` + Version string `json:"Version"` + Published time.Time `json:"Published"` + Artifacts []Artifact `json:"Artifacts"` +} + +func (bundle Bundle) downloadAndVerify(dataDir string) { + client := http.Client{} + for _, artifact := range bundle.Artifacts { + + filePath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + // TODO(vladimir): is this needed? + _ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm) + + // Check file is already downloaded and valid. + exists, err := checkIfFileIsValid(filePath, artifact) + if exists { + log.Debugf("file already download: %s", filePath) + continue + } else if err != nil { + log.Errorf("error while checking old download: %s", err) + } + + // Download artifact + err = processArtifact(&client, artifact, filePath) + if err != nil { + log.Errorf("updates: %s", err) + } + } +} + +func (bundle Bundle) Verify(dataDir string) error { + for _, artifact := range bundle.Artifacts { + artifactPath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + file, err := os.Open(artifactPath) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", artifactPath, err) + } + defer func() { _ = file.Close() }() + + isValid, err := checkIfFileIsValid(artifactPath, artifact) + if err != nil { + return err + } + + if !isValid { + return fmt.Errorf("file is not valid: %s", artifact.Filename) + } + } + + return nil +} + +func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { + // Check if file already exists + file, err := os.Open(filename) + if err != nil { + //nolint:nilerr + return false, nil + } + defer func() { _ = file.Close() }() + + providedHash, err := hex.DecodeString(artifact.SHA256) + if err != nil || len(providedHash) != sha256.Size { + return false, fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) + } + + // Calculate hash of the file + fileHash := sha256.New() + if _, err := io.Copy(fileHash, file); err != nil { + return false, fmt.Errorf("failed to read file: %w", err) + } + hashInBytes := fileHash.Sum(nil) + if !bytes.Equal(providedHash, hashInBytes) { + return false, fmt.Errorf("file exist but the hash does not match: %s", filename) + } + return true, nil +} + +func processArtifact(client *http.Client, artifact Artifact, filePath string) error { + providedHash, err := hex.DecodeString(artifact.SHA256) + if err != nil || len(providedHash) != sha256.Size { + return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) + } + + // Download + content, err := downloadFile(client, artifact.URLs) + if err != nil { + return fmt.Errorf("failed to download artifact: %w", err) + } + + // Decompress + if artifact.Unpack != "" { + content, err = unpack(artifact.Unpack, content) + if err != nil { + return fmt.Errorf("failed to decompress artifact: %w", err) + } + } + + // Verify + hash := sha256.Sum256(content) + if !bytes.Equal(providedHash, hash[:]) { + // FIXME(vladimir): just for testing. Make it an error before commit. + err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename) + log.Debugf("updates: %s", err) + } + + // Save + tmpFilename := fmt.Sprintf("%s.download", filePath) + file, err := os.Create(tmpFilename) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + _, err = file.Write(content) + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + + // Rename + err = os.Rename(tmpFilename, filePath) + if err != nil { + return fmt.Errorf("failed to rename file: %w", err) + } + + return nil +} + +func downloadFile(client *http.Client, urls []string) ([]byte, error) { + for _, url := range urls { + // Try to make the request + resp, err := client.Get(url) + if err != nil { + log.Warningf("failed a get file request to: %s", err) + continue + } + defer func() { _ = resp.Body.Close() }() + + // Check if the server returned an error + if resp.StatusCode != http.StatusOK { + log.Warningf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) + continue + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + log.Warningf("failed to read body of response: %s", err) + continue + } + return content, nil + } + + return nil, fmt.Errorf("failed to download file from the provided urls") +} + +func unpack(cType string, fileBytes []byte) ([]byte, error) { + switch cType { + case "zip": + { + return decompressZip(fileBytes) + } + case "gz": + { + return decompressGzip(fileBytes) + } + default: + { + return nil, fmt.Errorf("unsupported compression type") + } + } +} + +func decompressGzip(data []byte) ([]byte, error) { + // Create a gzip reader from the byte array + gzipReader, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { _ = gzipReader.Close() }() + + var buf bytes.Buffer + _, err = io.CopyN(&buf, gzipReader, MaxUnpackSize) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("failed to read gzip file: %w", err) + } + + return buf.Bytes(), nil +} + +func decompressZip(data []byte) ([]byte, error) { + // Create a zip reader from the byte array + zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return nil, fmt.Errorf("failed to create zip reader: %w", err) + } + + // Ensure there is only one file in the zip + if len(zipReader.File) != 1 { + return nil, fmt.Errorf("zip file must contain exactly one file") + } + + // Read the single file in the zip + file := zipReader.File[0] + fileReader, err := file.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file in zip: %w", err) + } + defer func() { _ = fileReader.Close() }() + + var buf bytes.Buffer + _, err = io.CopyN(&buf, fileReader, MaxUnpackSize) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("failed to read file in zip: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/service/updates/index.go b/service/updates/index.go new file mode 100644 index 00000000..adf4564f --- /dev/null +++ b/service/updates/index.go @@ -0,0 +1,110 @@ +package updates + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/safing/portmaster/base/log" +) + +type UpdateIndex struct { + Directory string + DownloadDirectory string + Ignore []string + IndexURLs []string + IndexFile string + AutoApply bool +} + +func (ui *UpdateIndex) downloadIndexFile() (err error) { + _ = os.MkdirAll(ui.Directory, defaultDirMode) + _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) + for _, url := range ui.IndexURLs { + err = ui.downloadIndexFileFromURL(url) + if err != nil { + log.Warningf("updates: %s", err) + continue + } + // Downloading was successful. + err = nil + break + } + return +} + +func (ui *UpdateIndex) checkForUpdates() (bool, error) { + err := ui.downloadIndexFile() + if err != nil { + return false, err + } + + currentBundle, err := ui.GetInstallBundle() + if err != nil { + return true, err // Current installed bundle not found, act as there is update. + } + updateBundle, err := ui.GetUpdateBundle() + if err != nil { + return false, err + } + + return currentBundle.Version != updateBundle.Version, nil +} + +func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { + client := http.Client{} + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed a get request to %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + return nil +} + +func (ui *UpdateIndex) GetInstallBundle() (*Bundle, error) { + indexFile := fmt.Sprintf("%s/%s", ui.Directory, ui.IndexFile) + return ui.GetBundle(indexFile) +} + +func (ui *UpdateIndex) GetUpdateBundle() (*Bundle, error) { + indexFile := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + return ui.GetBundle(indexFile) +} + +func (ui *UpdateIndex) GetBundle(indexFile string) (*Bundle, error) { + // Check if the file exists. + file, err := os.Open(indexFile) + if err != nil { + return nil, fmt.Errorf("failed to open index file: %w", err) + } + defer func() { _ = file.Close() }() + + // Read + content, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + // Parse + var bundle Bundle + err = json.Unmarshal(content, &bundle) + if err != nil { + return nil, err + } + + return &bundle, nil +} diff --git a/service/updates/main.go b/service/updates/main.go index bb942993..3e11064c 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -1,20 +1,14 @@ package updates import ( - "context" - "errors" - "flag" "fmt" - "net/url" "runtime" "time" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" ) const ( @@ -48,9 +42,6 @@ var ( userAgentFromFlag string updateServerFromFlag string - updateASAP bool - disableTaskSchedule bool - db = database.NewInterface(&database.Options{ Local: true, Internal: true, @@ -60,163 +51,126 @@ var ( // more context to requests made by the registry when // fetching resources from the update server. UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) - - // DefaultUpdateURLs defines the default base URLs of the update server. - DefaultUpdateURLs = []string{ - "https://updates.safing.io", - } - - // DisableSoftwareAutoUpdate specifies whether software updates should be disabled. - // This is used on Android, as it will never require binary updates. - DisableSoftwareAutoUpdate = false ) const ( - updatesDirName = "updates" - updateTaskRepeatDuration = 1 * time.Hour ) -func init() { - flag.StringVar(&updateServerFromFlag, "update-server", "", "set an alternative update server (full URL)") - flag.StringVar(&userAgentFromFlag, "update-agent", "", "set an alternative user agent for requests to the update server") -} - -func prep() error { - // Check if update server URL supplied via flag is a valid URL. - if updateServerFromFlag != "" { - u, err := url.Parse(updateServerFromFlag) - if err != nil { - return fmt.Errorf("supplied update server URL is invalid: %w", err) - } - if u.Scheme != "https" { - return errors.New("supplied update server URL must use HTTPS") - } - } - - if err := registerConfig(); err != nil { - return err - } - - return registerAPIEndpoints() -} - func start() error { - initConfig() + // module.restartWorkerMgr.Repeat(10 * time.Minute) + // module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig) - module.restartWorkerMgr.Repeat(10 * time.Minute) - module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig) + // // create registry + // registry = &updater.ResourceRegistry{ + // Name: ModuleName, + // UpdateURLs: DefaultUpdateURLs, + // UserAgent: UserAgent, + // MandatoryUpdates: helper.MandatoryUpdates(), + // AutoUnpack: helper.AutoUnpackUpdates(), + // Verification: helper.VerificationConfig, + // DevMode: devMode(), + // Online: true, + // } + // // Override values from flags. + // if userAgentFromFlag != "" { + // registry.UserAgent = userAgentFromFlag + // } + // if updateServerFromFlag != "" { + // registry.UpdateURLs = []string{updateServerFromFlag} + // } - // create registry - registry = &updater.ResourceRegistry{ - Name: ModuleName, - UpdateURLs: DefaultUpdateURLs, - UserAgent: UserAgent, - MandatoryUpdates: helper.MandatoryUpdates(), - AutoUnpack: helper.AutoUnpackUpdates(), - Verification: helper.VerificationConfig, - DevMode: devMode(), - Online: true, - } - // Override values from flags. - if userAgentFromFlag != "" { - registry.UserAgent = userAgentFromFlag - } - if updateServerFromFlag != "" { - registry.UpdateURLs = []string{updateServerFromFlag} - } - - // pre-init state - updateStateExport, err := LoadStateExport() - if err != nil { - log.Debugf("updates: failed to load exported update state: %s", err) - } else if updateStateExport.UpdateState != nil { - err := registry.PreInitUpdateState(*updateStateExport.UpdateState) - if err != nil { - return err - } - } + // // pre-init state + // updateStateExport, err := LoadStateExport() + // if err != nil { + // log.Debugf("updates: failed to load exported update state: %s", err) + // } else if updateStateExport.UpdateState != nil { + // err := registry.PreInitUpdateState(*updateStateExport.UpdateState) + // if err != nil { + // return err + // } + // } // initialize - err = registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) - if err != nil { - return err - } + // err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) + // if err != nil { + // return err + // } - // register state provider - err = registerRegistryStateProvider() - if err != nil { - return err - } - registry.StateNotifyFunc = pushRegistryState + // // register state provider + // err = registerRegistryStateProvider() + // if err != nil { + // return err + // } + // registry.StateNotifyFunc = pushRegistryState - // Set indexes based on the release channel. - warning := helper.SetIndexes( - registry, - initialReleaseChannel, - true, - enableSoftwareUpdates() && !DisableSoftwareAutoUpdate, - enableIntelUpdates(), - ) - if warning != nil { - log.Warningf("updates: %s", warning) - } + // // Set indexes based on the release channel. + // warning := helper.SetIndexes( + // registry, + // initialReleaseChannel, + // true, + // enableSoftwareUpdates() && !DisableSoftwareAutoUpdate, + // enableIntelUpdates(), + // ) + // if warning != nil { + // log.Warningf("updates: %s", warning) + // } - err = registry.LoadIndexes(module.m.Ctx()) - if err != nil { - log.Warningf("updates: failed to load indexes: %s", err) - } + // err = registry.LoadIndexes(module.m.Ctx()) + // if err != nil { + // log.Warningf("updates: failed to load indexes: %s", err) + // } - err = registry.ScanStorage("") - if err != nil { - log.Warningf("updates: error during storage scan: %s", err) - } + // err = registry.ScanStorage("") + // if err != nil { + // log.Warningf("updates: error during storage scan: %s", err) + // } - registry.SelectVersions() - module.EventVersionsUpdated.Submit(struct{}{}) + // registry.SelectVersions() + // module.EventVersionsUpdated.Submit(struct{}{}) - // Initialize the version export - this requires the registry to be set up. - err = initVersionExport() - if err != nil { - return err - } + // // Initialize the version export - this requires the registry to be set up. + // err = initVersionExport() + // if err != nil { + // return err + // } - // start updater task - if !disableTaskSchedule { - _ = module.updateWorkerMgr.Repeat(30 * time.Minute) - } + // // start updater task + // if !disableTaskSchedule { + // _ = module.updateWorkerMgr.Repeat(30 * time.Minute) + // } - if updateASAP { - module.updateWorkerMgr.Go() - } + // if updateASAP { + // module.updateWorkerMgr.Go() + // } - // react to upgrades - if err := initUpgrader(); err != nil { - return err - } + // // react to upgrades + // if err := initUpgrader(); err != nil { + // return err + // } - warnOnIncorrectParentPath() + // warnOnIncorrectParentPath() return nil } // TriggerUpdate queues the update task to execute ASAP. func TriggerUpdate(forceIndexCheck, downloadAll bool) error { - switch { - case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): - return errors.New("automatic updating is disabled") + // switch { + // case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): + // return errors.New("automatic updating is disabled") - default: - if forceIndexCheck { - forceCheck.Set() - } - if downloadAll { - forceDownload.Set() - } + // default: + // if forceIndexCheck { + // forceCheck.Set() + // } + // if downloadAll { + // forceDownload.Set() + // } - // If index check if forced, start quicker. - module.updateWorkerMgr.Go() - } + // // If index check if forced, start quicker. + // module.updateWorkerMgr.Go() + // } log.Debugf("updates: triggering update to run as soon as possible") return nil @@ -232,68 +186,66 @@ func DisableUpdateSchedule() error { // return errors.New("module already online") // } - disableTaskSchedule = true - return nil } func checkForUpdates(ctx *mgr.WorkerCtx) (err error) { // Set correct error if context was canceled. - defer func() { - select { - case <-ctx.Done(): - err = context.Canceled - default: - } - }() + // defer func() { + // select { + // case <-ctx.Done(): + // err = context.Canceled + // default: + // } + // }() - // Get flags. - forceIndexCheck := forceCheck.SetToIf(true, false) - downloadAll := forceDownload.SetToIf(true, false) + // // Get flags. + // forceIndexCheck := forceCheck.SetToIf(true, false) + // downloadAll := forceDownload.SetToIf(true, false) - // Check again if downloading updates is enabled, or forced. - if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() { - log.Warningf("updates: automatic updates are disabled") - return nil - } + // // Check again if downloading updates is enabled, or forced. + // if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() { + // log.Warningf("updates: automatic updates are disabled") + // return nil + // } - defer func() { - // Resolve any error and send success notification. - if err == nil { - log.Infof("updates: successfully checked for updates") - notifyUpdateSuccess(forceIndexCheck) - return - } + // defer func() { + // // Resolve any error and send success notification. + // if err == nil { + // log.Infof("updates: successfully checked for updates") + // notifyUpdateSuccess(forceIndexCheck) + // return + // } - // Log and notify error. - log.Errorf("updates: check failed: %s", err) - notifyUpdateCheckFailed(forceIndexCheck, err) - }() + // // Log and notify error. + // log.Errorf("updates: check failed: %s", err) + // notifyUpdateCheckFailed(forceIndexCheck, err) + // }() - if err = registry.UpdateIndexes(ctx.Ctx()); err != nil { - err = fmt.Errorf("failed to update indexes: %w", err) - return //nolint:nakedret // TODO: Would "return err" work with the defer? - } + // if err = registry.UpdateIndexes(ctx.Ctx()); err != nil { + // err = fmt.Errorf("failed to update indexes: %w", err) + // return //nolint:nakedret // TODO: Would "return err" work with the defer? + // } - err = registry.DownloadUpdates(ctx.Ctx(), downloadAll) - if err != nil { - err = fmt.Errorf("failed to download updates: %w", err) - return //nolint:nakedret // TODO: Would "return err" work with the defer? - } + // err = registry.DownloadUpdates(ctx.Ctx(), downloadAll) + // if err != nil { + // err = fmt.Errorf("failed to download updates: %w", err) + // return //nolint:nakedret // TODO: Would "return err" work with the defer? + // } - registry.SelectVersions() + // registry.SelectVersions() - // Unpack selected resources. - err = registry.UnpackResources() - if err != nil { - err = fmt.Errorf("failed to unpack updates: %w", err) - return //nolint:nakedret // TODO: Would "return err" work with the defer? - } + // // Unpack selected resources. + // err = registry.UnpackResources() + // if err != nil { + // err = fmt.Errorf("failed to unpack updates: %w", err) + // return //nolint:nakedret // TODO: Would "return err" work with the defer? + // } - // Purge old resources - registry.Purge(2) + // // Purge old resources + // registry.Purge(2) - module.EventResourcesUpdated.Submit(struct{}{}) + // module.EventResourcesUpdated.Submit(struct{}{}) return nil } @@ -314,5 +266,6 @@ func RootPath() string { // return "" // } - return registry.StorageDir().Path + // return registry.StorageDir().Path + return "" } diff --git a/service/updates/module.go b/service/updates/module.go index c7cae1a9..002e315e 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -2,14 +2,24 @@ package updates import ( "errors" + "fmt" + "os" + "path/filepath" + "strings" "sync/atomic" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" ) +const ( + defaultFileMode = os.FileMode(0o0644) + defaultDirMode = os.FileMode(0o0755) +) + // Updates provides access to released artifacts. type Updates struct { m *mgr.Manager @@ -21,6 +31,9 @@ type Updates struct { EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] + binUpdates UpdateIndex + intelUpdates UpdateIndex + instance instance } @@ -40,19 +53,84 @@ func New(instance instance) (*Updates, error) { m: m, states: m.NewStateMgr(), - updateWorkerMgr: m.NewWorkerMgr("updater", checkForUpdates, nil), - restartWorkerMgr: m.NewWorkerMgr("automatic restart", automaticRestart, nil), EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), instance: instance, } - if err := prep(); err != nil { - return nil, err + + // Events + module.updateWorkerMgr = m.NewWorkerMgr("updater", module.checkForUpdates, nil) + module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) + + module.binUpdates = UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/portmaster/new_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + module.intelUpdates = UpdateIndex{ + Directory: "/var/portmaster/intel", + DownloadDirectory: "/var/portmaster/new_intel", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, } return module, nil } +func deleteUnfinishedDownloads(rootDir string) error { + return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Check if the current file has the specified extension + if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { + log.Warningf("updates deleting unfinished: %s\n", path) + err := os.Remove(path) + if err != nil { + return fmt.Errorf("failed to delete file %s: %w", path, err) + } + } + + return nil + }) +} + +func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { + _ = deleteUnfinishedDownloads(u.binUpdates.DownloadDirectory) + hasUpdate, err := u.binUpdates.checkForUpdates() + if err != nil { + log.Warningf("failed to get binary index file: %s", err) + } + if hasUpdate { + binBundle, err := u.binUpdates.GetUpdateBundle() + if err == nil { + log.Debugf("Bin Bundle: %+v", binBundle) + _ = os.MkdirAll(u.binUpdates.DownloadDirectory, defaultDirMode) + binBundle.downloadAndVerify(u.binUpdates.DownloadDirectory) + } + } + _ = deleteUnfinishedDownloads(u.intelUpdates.DownloadDirectory) + hasUpdate, err = u.intelUpdates.checkForUpdates() + if err != nil { + log.Warningf("failed to get intel index file: %s", err) + } + if hasUpdate { + intelBundle, err := u.intelUpdates.GetUpdateBundle() + if err == nil { + log.Debugf("Intel Bundle: %+v", intelBundle) + _ = os.MkdirAll(u.intelUpdates.DownloadDirectory, defaultDirMode) + intelBundle.downloadAndVerify(u.intelUpdates.DownloadDirectory) + } + } + return nil +} + // States returns the state manager. func (u *Updates) States() *mgr.StateMgr { return u.states @@ -65,7 +143,36 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { - return start() + initConfig() + u.m.Go("check for updates", func(w *mgr.WorkerCtx) error { + binBundle, err := u.binUpdates.GetInstallBundle() + if err != nil { + log.Warningf("failed to get binary bundle: %s", err) + } else { + err = binBundle.Verify(u.binUpdates.Directory) + if err != nil { + log.Warningf("binary bundle is not valid: %s", err) + } else { + log.Infof("binary bundle is valid") + } + } + + intelBundle, err := u.intelUpdates.GetInstallBundle() + if err != nil { + log.Warningf("failed to get intel bundle: %s", err) + } else { + err = intelBundle.Verify(u.intelUpdates.Directory) + if err != nil { + log.Warningf("intel bundle is not valid: %s", err) + } else { + log.Infof("intel bundle is valid") + } + } + + return nil + }) + u.updateWorkerMgr.Go() + return nil } // Stop stops the module. diff --git a/service/updates/registry.go b/service/updates/registry.go new file mode 100644 index 00000000..de15a98b --- /dev/null +++ b/service/updates/registry.go @@ -0,0 +1 @@ +package updates diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go index 622b3909..e4685c3b 100644 --- a/service/updates/upgrader.go +++ b/service/updates/upgrader.go @@ -181,16 +181,16 @@ func upgradeHub() error { DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) // Increase update checks in order to detect aborts better. - if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(10 * time.Minute) - } + // if !disableTaskSchedule { + module.updateWorkerMgr.Repeat(10 * time.Minute) + // } } else { AbortRestart() // Set update task schedule back to normal. - if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) - } + // if !disableTaskSchedule { + module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) + // } } return nil From 9bae1afd73ef527a1989271d6223e4b49966b88c Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 16 Aug 2024 16:05:01 +0300 Subject: [PATCH 06/62] [WIP] New updater first working prototype --- base/updater/export.go | 22 +- base/updater/fetch.go | 622 +++++++------- base/updater/file.go | 222 +++-- base/updater/get.go | 119 ++- base/updater/notifier.go | 52 +- base/updater/registry.go | 434 +++++----- base/updater/resource.go | 978 +++++++++++------------ base/updater/signing.go | 76 +- base/updater/state.go | 282 +++---- base/updater/storage.go | 466 +++++------ base/updater/unpacking.go | 336 ++++---- base/updater/updating.go | 618 +++++++------- service/broadcasts/data.go | 24 +- service/broadcasts/module.go | 5 +- service/broadcasts/notify.go | 3 +- service/core/api.go | 2 +- service/intel/filterlists/database.go | 22 +- service/intel/filterlists/index.go | 26 +- service/intel/filterlists/updater.go | 74 +- service/intel/geoip/database.go | 31 +- service/netenv/main.go | 5 +- service/netenv/online-status.go | 3 +- service/network/api.go | 3 +- service/ui/module.go | 9 +- service/ui/serve.go | 7 +- service/updates/api.go | 276 +++---- service/updates/config.go | 274 +++---- service/updates/export.go | 405 +++++----- service/updates/get.go | 89 +-- service/updates/helper/electron.go | 91 +-- service/updates/helper/indexes.go | 232 +++--- service/updates/helper/signing.go | 72 +- service/updates/helper/updates.go | 156 ++-- service/updates/index.go | 110 --- service/updates/main.go | 213 +---- service/updates/module.go | 149 ++-- service/updates/notify.go | 256 +++--- service/updates/os_integration_linux.go | 351 ++++---- service/updates/registry.go | 1 - service/updates/{ => registry}/bundle.go | 38 +- service/updates/registry/index.go | 56 ++ service/updates/registry/registry.go | 245 ++++++ service/updates/restart.go | 22 +- service/updates/state.go | 74 +- service/updates/upgrader.go | 695 ++++++++-------- spn/captain/intel.go | 10 +- 46 files changed, 4107 insertions(+), 4149 deletions(-) delete mode 100644 service/updates/index.go delete mode 100644 service/updates/registry.go rename service/updates/{ => registry}/bundle.go (86%) create mode 100644 service/updates/registry/index.go create mode 100644 service/updates/registry/registry.go diff --git a/base/updater/export.go b/base/updater/export.go index 55b64a3f..51889bde 100644 --- a/base/updater/export.go +++ b/base/updater/export.go @@ -1,15 +1,15 @@ package updater -// Export exports the list of resources. -func (reg *ResourceRegistry) Export() map[string]*Resource { - reg.RLock() - defer reg.RUnlock() +// // Export exports the list of resources. +// func (reg *ResourceRegistry) Export() map[string]*Resource { +// reg.RLock() +// defer reg.RUnlock() - // copy the map - copiedResources := make(map[string]*Resource) - for key, val := range reg.resources { - copiedResources[key] = val.Export() - } +// // copy the map +// copiedResources := make(map[string]*Resource) +// for key, val := range reg.resources { +// copiedResources[key] = val.Export() +// } - return copiedResources -} +// return copiedResources +// } diff --git a/base/updater/fetch.go b/base/updater/fetch.go index f324709d..89456774 100644 --- a/base/updater/fetch.go +++ b/base/updater/fetch.go @@ -1,347 +1,347 @@ package updater -import ( - "bytes" - "context" - "errors" - "fmt" - "hash" - "io" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "time" +// import ( +// "bytes" +// "context" +// "errors" +// "fmt" +// "hash" +// "io" +// "net/http" +// "net/url" +// "os" +// "path" +// "path/filepath" +// "time" - "github.com/safing/jess/filesig" - "github.com/safing/jess/lhash" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils/renameio" -) +// "github.com/safing/jess/filesig" +// "github.com/safing/jess/lhash" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils/renameio" +// ) -func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { - // backoff when retrying - if tries > 0 { - select { - case <-ctx.Done(): - return nil // module is shutting down - case <-time.After(time.Duration(tries*tries) * time.Second): - } - } +// func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { +// // backoff when retrying +// if tries > 0 { +// select { +// case <-ctx.Done(): +// return nil // module is shutting down +// case <-time.After(time.Duration(tries*tries) * time.Second): +// } +// } - // check destination dir - dirPath := filepath.Dir(rv.storagePath()) - err := reg.storageDir.EnsureAbsPath(dirPath) - if err != nil { - return fmt.Errorf("could not create updates folder: %s", dirPath) - } +// // check destination dir +// dirPath := filepath.Dir(rv.storagePath()) +// err := reg.storageDir.EnsureAbsPath(dirPath) +// if err != nil { +// return fmt.Errorf("could not create updates folder: %s", dirPath) +// } - // If verification is enabled, download signature first. - var ( - verifiedHash *lhash.LabeledHash - sigFileData []byte - ) - if rv.resource.VerificationOptions != nil { - verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( - ctx, client, - rv.resource.VerificationOptions, - rv.versionedSigPath(), rv.SigningMetadata(), - tries, - ) - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("signature verification failed: %w", err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - } - } - } +// // If verification is enabled, download signature first. +// var ( +// verifiedHash *lhash.LabeledHash +// sigFileData []byte +// ) +// if rv.resource.VerificationOptions != nil { +// verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( +// ctx, client, +// rv.resource.VerificationOptions, +// rv.versionedSigPath(), rv.SigningMetadata(), +// tries, +// ) +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("signature verification failed: %w", err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// } +// } +// } - // open file for writing - atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) - if err != nil { - return fmt.Errorf("could not create temp file for download: %w", err) - } - defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway +// // open file for writing +// atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) +// if err != nil { +// return fmt.Errorf("could not create temp file for download: %w", err) +// } +// defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - // start file download - resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) - if err != nil { - return err - } - defer func() { - _ = resp.Body.Close() - }() +// // start file download +// resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) +// if err != nil { +// return err +// } +// defer func() { +// _ = resp.Body.Close() +// }() - // Write to the hasher at the same time, if needed. - var hasher hash.Hash - var writeDst io.Writer = atomicFile - if verifiedHash != nil { - hasher = verifiedHash.Algorithm().RawHasher() - writeDst = io.MultiWriter(hasher, atomicFile) - } +// // Write to the hasher at the same time, if needed. +// var hasher hash.Hash +// var writeDst io.Writer = atomicFile +// if verifiedHash != nil { +// hasher = verifiedHash.Algorithm().RawHasher() +// writeDst = io.MultiWriter(hasher, atomicFile) +// } - // Download and write file. - n, err := io.Copy(writeDst, resp.Body) - if err != nil { - return fmt.Errorf("failed to download %q: %w", downloadURL, err) - } - if resp.ContentLength != n { - return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) - } +// // Download and write file. +// n, err := io.Copy(writeDst, resp.Body) +// if err != nil { +// return fmt.Errorf("failed to download %q: %w", downloadURL, err) +// } +// if resp.ContentLength != n { +// return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) +// } - // Before file is finalized, check if hash, if available. - if hasher != nil { - downloadDigest := hasher.Sum(nil) - if verifiedHash.EqualRaw(downloadDigest) { - log.Infof("%s: verified signature of %s", reg.Name, downloadURL) - } else { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return errors.New("file does not match signed checksum") - case SignaturePolicyWarn: - log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) - case SignaturePolicyDisable: - log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) - } +// // Before file is finalized, check if hash, if available. +// if hasher != nil { +// downloadDigest := hasher.Sum(nil) +// if verifiedHash.EqualRaw(downloadDigest) { +// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) +// } else { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return errors.New("file does not match signed checksum") +// case SignaturePolicyWarn: +// log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) +// case SignaturePolicyDisable: +// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) +// } - // Reset hasher to signal that the sig should not be written. - hasher = nil - } - } +// // Reset hasher to signal that the sig should not be written. +// hasher = nil +// } +// } - // Write signature file, if we have one and if verification succeeded. - if len(sigFileData) > 0 && hasher != nil { - sigFilePath := rv.storagePath() + filesig.Extension - err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) - } - } - } +// // Write signature file, if we have one and if verification succeeded. +// if len(sigFileData) > 0 && hasher != nil { +// sigFilePath := rv.storagePath() + filesig.Extension +// err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) +// } +// } +// } - // finalize file - err = atomicFile.CloseAtomicallyReplace() - if err != nil { - return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) - } - // set permissions - if !onWindows { - // TODO: only set executable files to 0755, set other to 0644 - err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. - if err != nil { - log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) - } - } +// // finalize file +// err = atomicFile.CloseAtomicallyReplace() +// if err != nil { +// return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) +// } +// // set permissions +// if !onWindows { +// // TODO: only set executable files to 0755, set other to 0644 +// err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. +// if err != nil { +// log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) +// } +// } - log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) - return nil -} +// log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) +// return nil +// } -func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { - // backoff when retrying - if tries > 0 { - select { - case <-ctx.Done(): - return nil // module is shutting down - case <-time.After(time.Duration(tries*tries) * time.Second): - } - } +// func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { +// // backoff when retrying +// if tries > 0 { +// select { +// case <-ctx.Done(): +// return nil // module is shutting down +// case <-time.After(time.Duration(tries*tries) * time.Second): +// } +// } - // Check destination dir. - dirPath := filepath.Dir(rv.storagePath()) - err := reg.storageDir.EnsureAbsPath(dirPath) - if err != nil { - return fmt.Errorf("could not create updates folder: %s", dirPath) - } +// // Check destination dir. +// dirPath := filepath.Dir(rv.storagePath()) +// err := reg.storageDir.EnsureAbsPath(dirPath) +// if err != nil { +// return fmt.Errorf("could not create updates folder: %s", dirPath) +// } - // Download and verify the missing signature. - verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( - ctx, client, - rv.resource.VerificationOptions, - rv.versionedSigPath(), rv.SigningMetadata(), - tries, - ) - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("signature verification failed: %w", err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) - } - return nil - } +// // Download and verify the missing signature. +// verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( +// ctx, client, +// rv.resource.VerificationOptions, +// rv.versionedSigPath(), rv.SigningMetadata(), +// tries, +// ) +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("signature verification failed: %w", err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) +// } +// return nil +// } - // Check if the signature matches the resource file. - ok, err := verifiedHash.MatchesFile(rv.storagePath()) - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("error while verifying resource file: %w", err) - case SignaturePolicyWarn: - log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) - case SignaturePolicyDisable: - log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) - } - return nil - } - if !ok { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return errors.New("resource file does not match signed checksum") - case SignaturePolicyWarn: - log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) - case SignaturePolicyDisable: - log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) - } - return nil - } +// // Check if the signature matches the resource file. +// ok, err := verifiedHash.MatchesFile(rv.storagePath()) +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("error while verifying resource file: %w", err) +// case SignaturePolicyWarn: +// log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) +// case SignaturePolicyDisable: +// log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) +// } +// return nil +// } +// if !ok { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return errors.New("resource file does not match signed checksum") +// case SignaturePolicyWarn: +// log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) +// case SignaturePolicyDisable: +// log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) +// } +// return nil +// } - // Write signature file. - err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec - if err != nil { - switch rv.resource.VerificationOptions.DownloadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) - } - } +// // Write signature file. +// err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec +// if err != nil { +// switch rv.resource.VerificationOptions.DownloadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) +// } +// } - log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) - return nil -} +// log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) +// return nil +// } -func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { - // Download signature file. - resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) - if err != nil { - return nil, nil, err - } - defer func() { - _ = resp.Body.Close() - }() - sigFileData, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, err - } +// func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { +// // Download signature file. +// resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) +// if err != nil { +// return nil, nil, err +// } +// defer func() { +// _ = resp.Body.Close() +// }() +// sigFileData, err := io.ReadAll(resp.Body) +// if err != nil { +// return nil, nil, err +// } - // Extract all signatures. - sigs, err := filesig.ParseSigFile(sigFileData) - switch { - case len(sigs) == 0 && err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - case len(sigs) == 0: - return nil, nil, errors.New("no signatures found in signature file") - case err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - } +// // Extract all signatures. +// sigs, err := filesig.ParseSigFile(sigFileData) +// switch { +// case len(sigs) == 0 && err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// case len(sigs) == 0: +// return nil, nil, errors.New("no signatures found in signature file") +// case err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// } - // Verify all signatures. - var verifiedHash *lhash.LabeledHash - for _, sig := range sigs { - fd, err := filesig.VerifyFileData( - sig, - requiredMetadata, - verifOpts.TrustStore, - ) - if err != nil { - return nil, sigFileData, err - } +// // Verify all signatures. +// var verifiedHash *lhash.LabeledHash +// for _, sig := range sigs { +// fd, err := filesig.VerifyFileData( +// sig, +// requiredMetadata, +// verifOpts.TrustStore, +// ) +// if err != nil { +// return nil, sigFileData, err +// } - // Save or check verified hash. - if verifiedHash == nil { - verifiedHash = fd.FileHash() - } else if !fd.FileHash().Equal(verifiedHash) { - // Return an error if two valid hashes mismatch. - // For simplicity, all hash algorithms must be the same for now. - return nil, sigFileData, errors.New("file hashes from different signatures do not match") - } - } +// // Save or check verified hash. +// if verifiedHash == nil { +// verifiedHash = fd.FileHash() +// } else if !fd.FileHash().Equal(verifiedHash) { +// // Return an error if two valid hashes mismatch. +// // For simplicity, all hash algorithms must be the same for now. +// return nil, sigFileData, errors.New("file hashes from different signatures do not match") +// } +// } - return verifiedHash, sigFileData, nil -} +// return verifiedHash, sigFileData, nil +// } -func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { - // backoff when retrying - if tries > 0 { - select { - case <-ctx.Done(): - return nil, "", nil // module is shutting down - case <-time.After(time.Duration(tries*tries) * time.Second): - } - } +// func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { +// // backoff when retrying +// if tries > 0 { +// select { +// case <-ctx.Done(): +// return nil, "", nil // module is shutting down +// case <-time.After(time.Duration(tries*tries) * time.Second): +// } +// } - // start file download - resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) - if err != nil { - return nil, downloadURL, err - } - defer func() { - _ = resp.Body.Close() - }() +// // start file download +// resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) +// if err != nil { +// return nil, downloadURL, err +// } +// defer func() { +// _ = resp.Body.Close() +// }() - // download and write file - buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) - n, err := io.Copy(buf, resp.Body) - if err != nil { - return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) - } - if resp.ContentLength != n { - return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) - } +// // download and write file +// buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) +// n, err := io.Copy(buf, resp.Body) +// if err != nil { +// return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) +// } +// if resp.ContentLength != n { +// return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) +// } - return buf.Bytes(), downloadURL, nil -} +// return buf.Bytes(), downloadURL, nil +// } -func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { - // parse update URL - updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] - u, err := url.Parse(updateBaseURL) - if err != nil { - return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) - } - // add download path - u.Path = path.Join(u.Path, downloadPath) - // compile URL - downloadURL = u.String() +// func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { +// // parse update URL +// updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] +// u, err := url.Parse(updateBaseURL) +// if err != nil { +// return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) +// } +// // add download path +// u.Path = path.Join(u.Path, downloadPath) +// // compile URL +// downloadURL = u.String() - // create request - req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) - if err != nil { - return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) - } +// // create request +// req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) +// if err != nil { +// return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) +// } - // set user agent - if reg.UserAgent != "" { - req.Header.Set("User-Agent", reg.UserAgent) - } +// // set user agent +// if reg.UserAgent != "" { +// req.Header.Set("User-Agent", reg.UserAgent) +// } - // start request - resp, err = client.Do(req) - if err != nil { - return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) - } +// // start request +// resp, err = client.Do(req) +// if err != nil { +// return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) +// } - // check return code - if resp.StatusCode != http.StatusOK { - _ = resp.Body.Close() - return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) - } +// // check return code +// if resp.StatusCode != http.StatusOK { +// _ = resp.Body.Close() +// return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) +// } - return resp, downloadURL, err -} +// return resp, downloadURL, err +// } diff --git a/base/updater/file.go b/base/updater/file.go index 90b7d356..758f2a7b 100644 --- a/base/updater/file.go +++ b/base/updater/file.go @@ -1,105 +1,97 @@ package updater import ( - "errors" - "io" - "io/fs" - "os" - "strings" - semver "github.com/hashicorp/go-version" +// semver "github.com/hashicorp/go-version" - "github.com/safing/jess/filesig" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" ) // File represents a file from the update system. -type File struct { - resource *Resource - version *ResourceVersion - notifier *notifier - versionedPath string - storagePath string -} +// type File struct { +// resource *Resource +// version *ResourceVersion +// notifier *notifier +// versionedPath string +// storagePath string +// } -// Identifier returns the identifier of the file. -func (file *File) Identifier() string { - return file.resource.Identifier -} +// // Identifier returns the identifier of the file. +// func (file *File) Identifier() string { +// return file.resource.Identifier +// } -// Version returns the version of the file. -func (file *File) Version() string { - return file.version.VersionNumber -} +// // Version returns the version of the file. +// func (file *File) Version() string { +// return file.version.VersionNumber +// } -// SemVer returns the semantic version of the file. -func (file *File) SemVer() *semver.Version { - return file.version.semVer -} +// // SemVer returns the semantic version of the file. +// func (file *File) SemVer() *semver.Version { +// return file.version.semVer +// } -// EqualsVersion normalizes the given version and checks equality with semver. -func (file *File) EqualsVersion(version string) bool { - return file.version.EqualsVersion(version) -} +// // EqualsVersion normalizes the given version and checks equality with semver. +// func (file *File) EqualsVersion(version string) bool { +// return file.version.EqualsVersion(version) +// } -// Path returns the absolute filepath of the file. -func (file *File) Path() string { - return file.storagePath -} +// // Path returns the absolute filepath of the file. +// func (file *File) Path() string { +// return file.storagePath +// } -// SigningMetadata returns the metadata to be included in signatures. -func (file *File) SigningMetadata() map[string]string { - return map[string]string{ - "id": file.Identifier(), - "version": file.Version(), - } -} +// // SigningMetadata returns the metadata to be included in signatures. +// func (file *File) SigningMetadata() map[string]string { +// return map[string]string{ +// "id": file.Identifier(), +// "version": file.Version(), +// } +// } // Verify verifies the given file. -func (file *File) Verify() ([]*filesig.FileData, error) { - // Check if verification is configured. - if file.resource.VerificationOptions == nil { - return nil, ErrVerificationNotConfigured - } +// func (file *File) Verify() ([]*filesig.FileData, error) { +// // Check if verification is configured. +// if file.resource.VerificationOptions == nil { +// return nil, ErrVerificationNotConfigured +// } - // Verify file. - fileData, err := filesig.VerifyFile( - file.storagePath, - file.storagePath+filesig.Extension, - file.SigningMetadata(), - file.resource.VerificationOptions.TrustStore, - ) - if err != nil { - switch file.resource.VerificationOptions.DiskLoadPolicy { - case SignaturePolicyRequire: - return nil, err - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) - } - } +// // Verify file. +// fileData, err := filesig.VerifyFile( +// file.storagePath, +// file.storagePath+filesig.Extension, +// file.SigningMetadata(), +// file.resource.VerificationOptions.TrustStore, +// ) +// if err != nil { +// switch file.resource.VerificationOptions.DiskLoadPolicy { +// case SignaturePolicyRequire: +// return nil, err +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) +// } +// } - return fileData, nil -} +// return fileData, nil +// } // Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted. -func (file *File) Blacklist() error { - return file.resource.Blacklist(file.version.VersionNumber) -} +// func (file *File) Blacklist() error { +// return file.resource.Blacklist(file.version.VersionNumber) +// } // markActiveWithLocking marks the file as active, locking the resource in the process. -func (file *File) markActiveWithLocking() { - file.resource.Lock() - defer file.resource.Unlock() +// func (file *File) markActiveWithLocking() { +// file.resource.Lock() +// defer file.resource.Unlock() - // update last used version - if file.resource.ActiveVersion != file.version { - log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) - file.resource.ActiveVersion = file.version - } -} +// // update last used version +// if file.resource.ActiveVersion != file.version { +// log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) +// file.resource.ActiveVersion = file.version +// } +// } // Unpacker describes the function that is passed to // File.Unpack. It receives a reader to the compressed/packed @@ -107,50 +99,50 @@ func (file *File) markActiveWithLocking() { // unpacked file contents. If the returned reader implements // io.Closer it's close method is invoked when an error // or io.EOF is returned from Read(). -type Unpacker func(io.Reader) (io.Reader, error) +// type Unpacker func(io.Reader) (io.Reader, error) // Unpack returns the path to the unpacked version of file and // unpacks it on demand using unpacker. -func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { - path := strings.TrimSuffix(file.Path(), suffix) +// func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { +// path := strings.TrimSuffix(file.Path(), suffix) - if suffix == "" { - path += "-unpacked" - } +// if suffix == "" { +// path += "-unpacked" +// } - _, err := os.Stat(path) - if err == nil { - return path, nil - } +// _, err := os.Stat(path) +// if err == nil { +// return path, nil +// } - if !errors.Is(err, fs.ErrNotExist) { - return "", err - } +// if !errors.Is(err, fs.ErrNotExist) { +// return "", err +// } - f, err := os.Open(file.Path()) - if err != nil { - return "", err - } - defer func() { - _ = f.Close() - }() +// f, err := os.Open(file.Path()) +// if err != nil { +// return "", err +// } +// defer func() { +// _ = f.Close() +// }() - r, err := unpacker(f) - if err != nil { - return "", err - } +// r, err := unpacker(f) +// if err != nil { +// return "", err +// } - ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ - TempDir: file.resource.registry.TmpDir().Path, - }) +// ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ +// TempDir: file.resource.registry.TmpDir().Path, +// }) - if c, ok := r.(io.Closer); ok { - if err := c.Close(); err != nil && ioErr == nil { - // if ioErr is already set we ignore the error from - // closing the unpacker. - ioErr = err - } - } +// if c, ok := r.(io.Closer); ok { +// if err := c.Close(); err != nil && ioErr == nil { +// // if ioErr is already set we ignore the error from +// // closing the unpacker. +// ioErr = err +// } +// } - return path, ioErr -} +// return path, ioErr +// } diff --git a/base/updater/get.go b/base/updater/get.go index 7b54c8e6..365cb736 100644 --- a/base/updater/get.go +++ b/base/updater/get.go @@ -2,7 +2,6 @@ package updater import ( "errors" - "fmt" ) // Errors returned by the updater package. @@ -14,75 +13,75 @@ var ( // GetFile returns the selected (mostly newest) file with the given // identifier or an error, if it fails. -func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { - return nil, fmt.Errorf("invalid file: %s", identifier) - // reg.RLock() - // res, ok := reg.resources[identifier] - // reg.RUnlock() - // if !ok { - // return nil, ErrNotFound - // } +// func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { +// return nil, fmt.Errorf("invalid file: %s", identifier) +// reg.RLock() +// res, ok := reg.resources[identifier] +// reg.RUnlock() +// if !ok { +// return nil, ErrNotFound +// } - // file := res.GetFile() - // // check if file is available locally - // if file.version.Available { - // file.markActiveWithLocking() +// file := res.GetFile() +// // check if file is available locally +// if file.version.Available { +// file.markActiveWithLocking() - // // Verify file, if configured. - // _, err := file.Verify() - // if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { - // // TODO: If verification is required, try deleting the resource and downloading it again. - // return nil, fmt.Errorf("failed to verify file: %w", err) - // } +// // Verify file, if configured. +// _, err := file.Verify() +// if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { +// // TODO: If verification is required, try deleting the resource and downloading it again. +// return nil, fmt.Errorf("failed to verify file: %w", err) +// } - // return file, nil - // } +// return file, nil +// } - // // check if online - // if !reg.Online { - // return nil, ErrNotAvailableLocally - // } +// // check if online +// if !reg.Online { +// return nil, ErrNotAvailableLocally +// } - // // check download dir - // err := reg.tmpDir.Ensure() - // if err != nil { - // return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) - // } +// // check download dir +// err := reg.tmpDir.Ensure() +// if err != nil { +// return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) +// } - // // Start registry operation. - // reg.state.StartOperation(StateFetching) - // defer reg.state.EndOperation() +// // Start registry operation. +// reg.state.StartOperation(StateFetching) +// defer reg.state.EndOperation() - // // download file - // log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) - // client := &http.Client{} - // for tries := range 5 { - // err = reg.fetchFile(context.TODO(), client, file.version, tries) - // if err != nil { - // log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) - // } else { - // file.markActiveWithLocking() +// // download file +// log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) +// client := &http.Client{} +// for tries := range 5 { +// err = reg.fetchFile(context.TODO(), client, file.version, tries) +// if err != nil { +// log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) +// } else { +// file.markActiveWithLocking() - // // TODO: We just download the file - should we verify it again? - // return file, nil - // } - // } - // log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) - // return nil, err -} +// // TODO: We just download the file - should we verify it again? +// return file, nil +// } +// } +// log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) +// return nil, err +// } // GetVersion returns the selected version of the given identifier. // The returned resource version may not be modified. -func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { - reg.RLock() - res, ok := reg.resources[identifier] - reg.RUnlock() - if !ok { - return nil, ErrNotFound - } +// func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { +// reg.RLock() +// res, ok := reg.resources[identifier] +// reg.RUnlock() +// if !ok { +// return nil, ErrNotFound +// } - res.Lock() - defer res.Unlock() +// res.Lock() +// defer res.Unlock() - return res.SelectedVersion, nil -} +// return res.SelectedVersion, nil +// } diff --git a/base/updater/notifier.go b/base/updater/notifier.go index 66b2832d..75de01e4 100644 --- a/base/updater/notifier.go +++ b/base/updater/notifier.go @@ -1,33 +1,33 @@ package updater -import ( - "github.com/tevino/abool" -) +// import ( +// "github.com/tevino/abool" +// ) -type notifier struct { - upgradeAvailable *abool.AtomicBool - notifyChannel chan struct{} -} +// type notifier struct { +// upgradeAvailable *abool.AtomicBool +// notifyChannel chan struct{} +// } -func newNotifier() *notifier { - return ¬ifier{ - upgradeAvailable: abool.NewBool(false), - notifyChannel: make(chan struct{}), - } -} +// func newNotifier() *notifier { +// return ¬ifier{ +// upgradeAvailable: abool.NewBool(false), +// notifyChannel: make(chan struct{}), +// } +// } -func (n *notifier) markAsUpgradeable() { - if n.upgradeAvailable.SetToIf(false, true) { - close(n.notifyChannel) - } -} +// func (n *notifier) markAsUpgradeable() { +// if n.upgradeAvailable.SetToIf(false, true) { +// close(n.notifyChannel) +// } +// } -// UpgradeAvailable returns whether an upgrade is available for this file. -func (file *File) UpgradeAvailable() bool { - return file.notifier.upgradeAvailable.IsSet() -} +// // UpgradeAvailable returns whether an upgrade is available for this file. +// func (file *File) UpgradeAvailable() bool { +// return file.notifier.upgradeAvailable.IsSet() +// } -// WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. -func (file *File) WaitForAvailableUpgrade() <-chan struct{} { - return file.notifier.notifyChannel -} +// // WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. +// func (file *File) WaitForAvailableUpgrade() <-chan struct{} { +// return file.notifier.notifyChannel +// } diff --git a/base/updater/registry.go b/base/updater/registry.go index 8deda74e..3d4861bf 100644 --- a/base/updater/registry.go +++ b/base/updater/registry.go @@ -1,270 +1,270 @@ package updater -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "sync" +// import ( +// "errors" +// "fmt" +// "os" +// "path/filepath" +// "runtime" +// "strings" +// "sync" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) -const ( - onWindows = runtime.GOOS == "windows" -) +// const ( +// onWindows = runtime.GOOS == "windows" +// ) // ResourceRegistry is a registry for managing update resources. -type ResourceRegistry struct { - sync.RWMutex +// type ResourceRegistry struct { +// sync.RWMutex - Name string - storageDir *utils.DirStructure - tmpDir *utils.DirStructure - indexes []*Index - state *RegistryState +// Name string +// storageDir *utils.DirStructure +// tmpDir *utils.DirStructure +// indexes []*Index +// state *RegistryState - resources map[string]*Resource - UpdateURLs []string - UserAgent string - MandatoryUpdates []string - AutoUnpack []string +// resources map[string]*Resource +// UpdateURLs []string +// UserAgent string +// MandatoryUpdates []string +// AutoUnpack []string - // Verification holds a map of VerificationOptions assigned to their - // applicable identifier path prefix. - // Use an empty string to denote the default. - // Use empty options to disable verification for a path prefix. - Verification map[string]*VerificationOptions +// // Verification holds a map of VerificationOptions assigned to their +// // applicable identifier path prefix. +// // Use an empty string to denote the default. +// // Use empty options to disable verification for a path prefix. +// Verification map[string]*VerificationOptions - // UsePreReleases signifies that pre-releases should be used when selecting a - // version. Even if false, a pre-release version will still be used if it is - // defined as the current version by an index. - UsePreReleases bool +// // UsePreReleases signifies that pre-releases should be used when selecting a +// // version. Even if false, a pre-release version will still be used if it is +// // defined as the current version by an index. +// UsePreReleases bool - // DevMode specifies if a local 0.0.0 version should be always chosen, when available. - DevMode bool +// // DevMode specifies if a local 0.0.0 version should be always chosen, when available. +// DevMode bool - // Online specifies if resources may be downloaded if not available locally. - Online bool +// // Online specifies if resources may be downloaded if not available locally. +// Online bool - // StateNotifyFunc may be set to receive any changes to the registry state. - // The specified function may lock the state, but may not block or take a - // lot of time. - StateNotifyFunc func(*RegistryState) -} +// // StateNotifyFunc may be set to receive any changes to the registry state. +// // The specified function may lock the state, but may not block or take a +// // lot of time. +// StateNotifyFunc func(*RegistryState) +// } -// AddIndex adds a new index to the resource registry. -// The order is important, as indexes added later will override the current -// release from earlier indexes. -func (reg *ResourceRegistry) AddIndex(idx Index) { - reg.Lock() - defer reg.Unlock() +// // AddIndex adds a new index to the resource registry. +// // The order is important, as indexes added later will override the current +// // release from earlier indexes. +// func (reg *ResourceRegistry) AddIndex(idx Index) { +// reg.Lock() +// defer reg.Unlock() - // Get channel name from path. - idx.Channel = strings.TrimSuffix( - filepath.Base(idx.Path), filepath.Ext(idx.Path), - ) +// // Get channel name from path. +// idx.Channel = strings.TrimSuffix( +// filepath.Base(idx.Path), filepath.Ext(idx.Path), +// ) - reg.indexes = append(reg.indexes, &idx) -} +// reg.indexes = append(reg.indexes, &idx) +// } -// PreInitUpdateState sets the initial update state of the registry before initialization. -func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { - if reg.state != nil { - return errors.New("registry already initialized") - } +// // PreInitUpdateState sets the initial update state of the registry before initialization. +// func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { +// if reg.state != nil { +// return errors.New("registry already initialized") +// } - reg.state = &RegistryState{ - Updates: s, - } - return nil -} +// reg.state = &RegistryState{ +// Updates: s, +// } +// return nil +// } -// Initialize initializes a raw registry struct and makes it ready for usage. -func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { - // check if storage dir is available - err := storageDir.Ensure() - if err != nil { - return err - } +// // Initialize initializes a raw registry struct and makes it ready for usage. +// func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { +// // check if storage dir is available +// err := storageDir.Ensure() +// if err != nil { +// return err +// } - // set default name - if reg.Name == "" { - reg.Name = "updater" - } +// // set default name +// if reg.Name == "" { +// reg.Name = "updater" +// } - // initialize private attributes - reg.storageDir = storageDir - reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) - reg.resources = make(map[string]*Resource) - if reg.state == nil { - reg.state = &RegistryState{} - } - reg.state.ID = StateReady - reg.state.reg = reg +// // initialize private attributes +// reg.storageDir = storageDir +// reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) +// reg.resources = make(map[string]*Resource) +// if reg.state == nil { +// reg.state = &RegistryState{} +// } +// reg.state.ID = StateReady +// reg.state.reg = reg - // remove tmp dir to delete old entries - err = reg.Cleanup() - if err != nil { - log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) - } +// // remove tmp dir to delete old entries +// err = reg.Cleanup() +// if err != nil { +// log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) +// } - // (re-)create tmp dir - err = reg.tmpDir.Ensure() - if err != nil { - log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) - } +// // (re-)create tmp dir +// err = reg.tmpDir.Ensure() +// if err != nil { +// log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) +// } - // Check verification options. - if reg.Verification != nil { - for prefix, opts := range reg.Verification { - // Check if verification is disable for this prefix. - if opts == nil { - continue - } +// // Check verification options. +// if reg.Verification != nil { +// for prefix, opts := range reg.Verification { +// // Check if verification is disable for this prefix. +// if opts == nil { +// continue +// } - // If enabled, a trust store is required. - if opts.TrustStore == nil { - return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) - } +// // If enabled, a trust store is required. +// if opts.TrustStore == nil { +// return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) +// } - // DownloadPolicy must be equal or stricter than DiskLoadPolicy. - if opts.DiskLoadPolicy < opts.DownloadPolicy { - return errors.New("verification download policy must be equal or stricter than the disk load policy") - } +// // DownloadPolicy must be equal or stricter than DiskLoadPolicy. +// if opts.DiskLoadPolicy < opts.DownloadPolicy { +// return errors.New("verification download policy must be equal or stricter than the disk load policy") +// } - // Warn if all policies are disabled. - if opts.DownloadPolicy == SignaturePolicyDisable && - opts.DiskLoadPolicy == SignaturePolicyDisable { - log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) - } - } - } +// // Warn if all policies are disabled. +// if opts.DownloadPolicy == SignaturePolicyDisable && +// opts.DiskLoadPolicy == SignaturePolicyDisable { +// log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) +// } +// } +// } - return nil -} +// return nil +// } -// StorageDir returns the main storage dir of the resource registry. -func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { - return reg.storageDir -} +// // StorageDir returns the main storage dir of the resource registry. +// func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { +// return reg.storageDir +// } -// TmpDir returns the temporary working dir of the resource registry. -func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { - return reg.tmpDir -} +// // TmpDir returns the temporary working dir of the resource registry. +// func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { +// return reg.tmpDir +// } -// SetDevMode sets the development mode flag. -func (reg *ResourceRegistry) SetDevMode(on bool) { - reg.Lock() - defer reg.Unlock() +// // SetDevMode sets the development mode flag. +// func (reg *ResourceRegistry) SetDevMode(on bool) { +// reg.Lock() +// defer reg.Unlock() - reg.DevMode = on -} +// reg.DevMode = on +// } -// SetUsePreReleases sets the UsePreReleases flag. -func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { - reg.Lock() - defer reg.Unlock() +// // SetUsePreReleases sets the UsePreReleases flag. +// func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { +// reg.Lock() +// defer reg.Unlock() - reg.UsePreReleases = yes -} +// reg.UsePreReleases = yes +// } -// AddResource adds a resource to the registry. Does _not_ select new version. -func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { - reg.Lock() - defer reg.Unlock() +// // AddResource adds a resource to the registry. Does _not_ select new version. +// func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { +// reg.Lock() +// defer reg.Unlock() - err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) - return err -} +// err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) +// return err +// } -func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { - res, ok := reg.resources[identifier] - if !ok { - res = reg.newResource(identifier) - reg.resources[identifier] = res - } - res.Index = index +// func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { +// res, ok := reg.resources[identifier] +// if !ok { +// res = reg.newResource(identifier) +// reg.resources[identifier] = res +// } +// res.Index = index - return res.AddVersion(version, available, currentRelease, preRelease) -} +// return res.AddVersion(version, available, currentRelease, preRelease) +// } -// AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. -func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { - reg.Lock() - defer reg.Unlock() +// // AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. +// func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { +// reg.Lock() +// defer reg.Unlock() - // add versions and their flags to registry - var lastError error - for identifier, version := range versions { - lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) - if lastError != nil { - log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) - } - } +// // add versions and their flags to registry +// var lastError error +// for identifier, version := range versions { +// lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) +// if lastError != nil { +// log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) +// } +// } - return lastError -} +// return lastError +// } -// SelectVersions selects new resource versions depending on the current registry state. -func (reg *ResourceRegistry) SelectVersions() { - reg.RLock() - defer reg.RUnlock() +// // SelectVersions selects new resource versions depending on the current registry state. +// func (reg *ResourceRegistry) SelectVersions() { +// reg.RLock() +// defer reg.RUnlock() - for _, res := range reg.resources { - res.Lock() - res.selectVersion() - res.Unlock() - } -} +// for _, res := range reg.resources { +// res.Lock() +// res.selectVersion() +// res.Unlock() +// } +// } -// GetSelectedVersions returns a list of the currently selected versions. -func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { - reg.RLock() - defer reg.RUnlock() +// // GetSelectedVersions returns a list of the currently selected versions. +// func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { +// reg.RLock() +// defer reg.RUnlock() - for _, res := range reg.resources { - res.Lock() - versions[res.Identifier] = res.SelectedVersion.VersionNumber - res.Unlock() - } +// for _, res := range reg.resources { +// res.Lock() +// versions[res.Identifier] = res.SelectedVersion.VersionNumber +// res.Unlock() +// } - return -} +// return +// } -// Purge deletes old updates, retaining a certain amount, specified by the keep -// parameter. Will at least keep 2 updates per resource. -func (reg *ResourceRegistry) Purge(keep int) { - reg.RLock() - defer reg.RUnlock() +// // Purge deletes old updates, retaining a certain amount, specified by the keep +// // parameter. Will at least keep 2 updates per resource. +// func (reg *ResourceRegistry) Purge(keep int) { +// reg.RLock() +// defer reg.RUnlock() - for _, res := range reg.resources { - res.Purge(keep) - } -} +// for _, res := range reg.resources { +// res.Purge(keep) +// } +// } -// ResetResources removes all resources from the registry. -func (reg *ResourceRegistry) ResetResources() { - reg.Lock() - defer reg.Unlock() +// // ResetResources removes all resources from the registry. +// func (reg *ResourceRegistry) ResetResources() { +// reg.Lock() +// defer reg.Unlock() - reg.resources = make(map[string]*Resource) -} +// reg.resources = make(map[string]*Resource) +// } -// ResetIndexes removes all indexes from the registry. -func (reg *ResourceRegistry) ResetIndexes() { - reg.Lock() - defer reg.Unlock() +// // ResetIndexes removes all indexes from the registry. +// func (reg *ResourceRegistry) ResetIndexes() { +// reg.Lock() +// defer reg.Unlock() - reg.indexes = make([]*Index, 0, len(reg.indexes)) -} +// reg.indexes = make([]*Index, 0, len(reg.indexes)) +// } -// Cleanup removes temporary files. -func (reg *ResourceRegistry) Cleanup() error { - // delete download tmp dir - return os.RemoveAll(reg.tmpDir.Path) -} +// // Cleanup removes temporary files. +// func (reg *ResourceRegistry) Cleanup() error { +// // delete download tmp dir +// return os.RemoveAll(reg.tmpDir.Path) +// } diff --git a/base/updater/resource.go b/base/updater/resource.go index 325f70cc..9180d0c9 100644 --- a/base/updater/resource.go +++ b/base/updater/resource.go @@ -1,582 +1,582 @@ package updater -import ( - "errors" - "io/fs" - "os" - "path/filepath" - "sort" - "strings" - "sync" +// import ( +// "errors" +// "io/fs" +// "os" +// "path/filepath" +// "sort" +// "strings" +// "sync" - semver "github.com/hashicorp/go-version" +// semver "github.com/hashicorp/go-version" - "github.com/safing/jess/filesig" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) +// "github.com/safing/jess/filesig" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) -var devVersion *semver.Version +// var devVersion *semver.Version -func init() { - var err error - devVersion, err = semver.NewVersion("0") - if err != nil { - panic(err) - } -} +// func init() { +// var err error +// devVersion, err = semver.NewVersion("0") +// if err != nil { +// panic(err) +// } +// } // Resource represents a resource (via an identifier) and multiple file versions. -type Resource struct { - sync.Mutex - registry *ResourceRegistry - notifier *notifier +// type Resource struct { +// sync.Mutex +// registry *ResourceRegistry +// notifier *notifier - // Identifier is the unique identifier for that resource. - // It forms a file path using a forward-slash as the - // path separator. - Identifier string +// // Identifier is the unique identifier for that resource. +// // It forms a file path using a forward-slash as the +// // path separator. +// Identifier string - // Versions holds all available resource versions. - Versions []*ResourceVersion +// // Versions holds all available resource versions. +// Versions []*ResourceVersion - // ActiveVersion is the last version of the resource - // that someone requested using GetFile(). - ActiveVersion *ResourceVersion +// // ActiveVersion is the last version of the resource +// // that someone requested using GetFile(). +// ActiveVersion *ResourceVersion - // SelectedVersion is newest, selectable version of - // that resource that is available. A version - // is selectable if it's not blacklisted by the user. - // Note that it's not guaranteed that the selected version - // is available locally. In that case, GetFile will attempt - // to download the latest version from the updates servers - // specified in the resource registry. - SelectedVersion *ResourceVersion +// // SelectedVersion is newest, selectable version of +// // that resource that is available. A version +// // is selectable if it's not blacklisted by the user. +// // Note that it's not guaranteed that the selected version +// // is available locally. In that case, GetFile will attempt +// // to download the latest version from the updates servers +// // specified in the resource registry. +// SelectedVersion *ResourceVersion - // VerificationOptions holds the verification options for this resource. - VerificationOptions *VerificationOptions +// // VerificationOptions holds the verification options for this resource. +// VerificationOptions *VerificationOptions - // Index holds a reference to the index this resource was last defined in. - // Will be nil if resource was only found on disk. - Index *Index -} +// // Index holds a reference to the index this resource was last defined in. +// // Will be nil if resource was only found on disk. +// Index *Index +// } // ResourceVersion represents a single version of a resource. -type ResourceVersion struct { - resource *Resource +// type ResourceVersion struct { +// resource *Resource - // VersionNumber is the string representation of the resource - // version. - VersionNumber string - semVer *semver.Version +// // VersionNumber is the string representation of the resource +// // version. +// VersionNumber string +// semVer *semver.Version - // Available indicates if this version is available locally. - Available bool +// // Available indicates if this version is available locally. +// Available bool - // SigAvailable indicates if the signature of this version is available locally. - SigAvailable bool +// // SigAvailable indicates if the signature of this version is available locally. +// SigAvailable bool - // CurrentRelease indicates that this is the current release that should be - // selected, if possible. - CurrentRelease bool +// // CurrentRelease indicates that this is the current release that should be +// // selected, if possible. +// CurrentRelease bool - // PreRelease indicates that this version is pre-release. - PreRelease bool +// // PreRelease indicates that this version is pre-release. +// PreRelease bool - // Blacklisted may be set to true if this version should - // be skipped and not used. This is useful if the version - // is known to be broken. - Blacklisted bool -} +// // Blacklisted may be set to true if this version should +// // be skipped and not used. This is useful if the version +// // is known to be broken. +// Blacklisted bool +// } -func (rv *ResourceVersion) String() string { - return rv.VersionNumber -} +// func (rv *ResourceVersion) String() string { +// return rv.VersionNumber +// } -// SemVer returns the semantic version of the resource. -func (rv *ResourceVersion) SemVer() *semver.Version { - return rv.semVer -} +// // SemVer returns the semantic version of the resource. +// func (rv *ResourceVersion) SemVer() *semver.Version { +// return rv.semVer +// } // EqualsVersion normalizes the given version and checks equality with semver. -func (rv *ResourceVersion) EqualsVersion(version string) bool { - cmpSemVer, err := semver.NewVersion(version) - if err != nil { - return false - } +// func (rv *ResourceVersion) EqualsVersion(version string) bool { +// cmpSemVer, err := semver.NewVersion(version) +// if err != nil { +// return false +// } - return rv.semVer.Equal(cmpSemVer) -} +// return rv.semVer.Equal(cmpSemVer) +// } -// isSelectable returns true if the version represented by rv is selectable. -// A version is selectable if it's not blacklisted and either already locally -// available or ready to be downloaded. -func (rv *ResourceVersion) isSelectable() bool { - switch { - case rv.Blacklisted: - // Should not be used. - return false - case rv.Available: - // Is available locally, use! - return true - case !rv.resource.registry.Online: - // Cannot download, because registry is set to offline. - return false - case rv.resource.Index == nil: - // Cannot download, because resource is not part of an index. - return false - case !rv.resource.Index.AutoDownload: - // Cannot download, because index may not automatically download. - return false - default: - // Is not available locally, but we are allowed to download it on request! - return true - } -} +// // isSelectable returns true if the version represented by rv is selectable. +// // A version is selectable if it's not blacklisted and either already locally +// // available or ready to be downloaded. +// func (rv *ResourceVersion) isSelectable() bool { +// switch { +// case rv.Blacklisted: +// // Should not be used. +// return false +// case rv.Available: +// // Is available locally, use! +// return true +// case !rv.resource.registry.Online: +// // Cannot download, because registry is set to offline. +// return false +// case rv.resource.Index == nil: +// // Cannot download, because resource is not part of an index. +// return false +// case !rv.resource.Index.AutoDownload: +// // Cannot download, because index may not automatically download. +// return false +// default: +// // Is not available locally, but we are allowed to download it on request! +// return true +// } +// } // isBetaVersionNumber checks if rv is marked as a beta version by checking // the version string. It does not honor the BetaRelease field of rv! -func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused - // "b" suffix check if for backwards compatibility - // new versions should use the pre-release suffix as - // declared by https://semver.org - // i.e. 1.2.3-beta - switch rv.semVer.Prerelease() { - case "b", "beta": - return true - default: - return false - } -} +// func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused +// // "b" suffix check if for backwards compatibility +// // new versions should use the pre-release suffix as +// // declared by https://semver.org +// // i.e. 1.2.3-beta +// switch rv.semVer.Prerelease() { +// case "b", "beta": +// return true +// default: +// return false +// } +// } // Export makes a copy of the resource with only the exposed information. // Attributes are copied and safe to access. // Any ResourceVersion must not be modified. -func (res *Resource) Export() *Resource { - res.Lock() - defer res.Unlock() +// func (res *Resource) Export() *Resource { +// res.Lock() +// defer res.Unlock() - // Copy attibutes. - export := &Resource{ - Identifier: res.Identifier, - Versions: make([]*ResourceVersion, len(res.Versions)), - ActiveVersion: res.ActiveVersion, - SelectedVersion: res.SelectedVersion, - } - // Copy Versions slice. - copy(export.Versions, res.Versions) +// // Copy attibutes. +// export := &Resource{ +// Identifier: res.Identifier, +// Versions: make([]*ResourceVersion, len(res.Versions)), +// ActiveVersion: res.ActiveVersion, +// SelectedVersion: res.SelectedVersion, +// } +// // Copy Versions slice. +// copy(export.Versions, res.Versions) - return export -} +// return export +// } -// Len is the number of elements in the collection. -// It implements sort.Interface for ResourceVersion. -func (res *Resource) Len() int { - return len(res.Versions) -} +// // Len is the number of elements in the collection. +// // It implements sort.Interface for ResourceVersion. +// func (res *Resource) Len() int { +// return len(res.Versions) +// } -// Less reports whether the element with index i should -// sort before the element with index j. -// It implements sort.Interface for ResourceVersions. -func (res *Resource) Less(i, j int) bool { - return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer) -} +// // Less reports whether the element with index i should +// // sort before the element with index j. +// // It implements sort.Interface for ResourceVersions. +// func (res *Resource) Less(i, j int) bool { +// return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer) +// } -// Swap swaps the elements with indexes i and j. -// It implements sort.Interface for ResourceVersions. -func (res *Resource) Swap(i, j int) { - res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i] -} +// // Swap swaps the elements with indexes i and j. +// // It implements sort.Interface for ResourceVersions. +// func (res *Resource) Swap(i, j int) { +// res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i] +// } -// available returns whether any version of the resource is available. -func (res *Resource) available() bool { - for _, rv := range res.Versions { - if rv.Available { - return true - } - } - return false -} +// // available returns whether any version of the resource is available. +// func (res *Resource) available() bool { +// for _, rv := range res.Versions { +// if rv.Available { +// return true +// } +// } +// return false +// } -// inUse returns true if the resource is currently in use. -func (res *Resource) inUse() bool { - return res.ActiveVersion != nil -} +// // inUse returns true if the resource is currently in use. +// func (res *Resource) inUse() bool { +// return res.ActiveVersion != nil +// } -// AnyVersionAvailable returns true if any version of -// res is locally available. -func (res *Resource) AnyVersionAvailable() bool { - res.Lock() - defer res.Unlock() +// // AnyVersionAvailable returns true if any version of +// // res is locally available. +// func (res *Resource) AnyVersionAvailable() bool { +// res.Lock() +// defer res.Unlock() - return res.available() -} +// return res.available() +// } -func (reg *ResourceRegistry) newResource(identifier string) *Resource { - return &Resource{ - registry: reg, - Identifier: identifier, - Versions: make([]*ResourceVersion, 0, 1), - VerificationOptions: reg.GetVerificationOptions(identifier), - } -} +// func (reg *ResourceRegistry) newResource(identifier string) *Resource { +// return &Resource{ +// registry: reg, +// Identifier: identifier, +// Versions: make([]*ResourceVersion, 0, 1), +// VerificationOptions: reg.GetVerificationOptions(identifier), +// } +// } -// AddVersion adds a resource version to a resource. -func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error { - res.Lock() - defer res.Unlock() +// // AddVersion adds a resource version to a resource. +// func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error { +// res.Lock() +// defer res.Unlock() - // reset current release flags - if currentRelease { - for _, rv := range res.Versions { - rv.CurrentRelease = false - } - } +// // reset current release flags +// if currentRelease { +// for _, rv := range res.Versions { +// rv.CurrentRelease = false +// } +// } - var rv *ResourceVersion - // check for existing version - for _, possibleMatch := range res.Versions { - if possibleMatch.VersionNumber == version { - rv = possibleMatch - break - } - } +// var rv *ResourceVersion +// // check for existing version +// for _, possibleMatch := range res.Versions { +// if possibleMatch.VersionNumber == version { +// rv = possibleMatch +// break +// } +// } - // create new version if none found - if rv == nil { - // parse to semver - sv, err := semver.NewVersion(version) - if err != nil { - return err - } +// // create new version if none found +// if rv == nil { +// // parse to semver +// sv, err := semver.NewVersion(version) +// if err != nil { +// return err +// } - rv = &ResourceVersion{ - resource: res, - VersionNumber: sv.String(), // Use normalized version. - semVer: sv, - } - res.Versions = append(res.Versions, rv) - } +// rv = &ResourceVersion{ +// resource: res, +// VersionNumber: sv.String(), // Use normalized version. +// semVer: sv, +// } +// res.Versions = append(res.Versions, rv) +// } - // set flags - if available { - rv.Available = true +// // set flags +// if available { +// rv.Available = true - // If available and signatures are enabled for this resource, check if the - // signature is available. - if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) { - rv.SigAvailable = true - } - } - if currentRelease { - rv.CurrentRelease = true - } - if preRelease || rv.semVer.Prerelease() != "" { - rv.PreRelease = true - } +// // If available and signatures are enabled for this resource, check if the +// // signature is available. +// if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) { +// rv.SigAvailable = true +// } +// } +// if currentRelease { +// rv.CurrentRelease = true +// } +// if preRelease || rv.semVer.Prerelease() != "" { +// rv.PreRelease = true +// } - return nil -} +// return nil +// } -// GetFile returns the selected version as a *File. -func (res *Resource) GetFile() *File { - res.Lock() - defer res.Unlock() +// // GetFile returns the selected version as a *File. +// func (res *Resource) GetFile() *File { +// res.Lock() +// defer res.Unlock() - // check for notifier - if res.notifier == nil { - // create new notifier - res.notifier = newNotifier() - } +// // check for notifier +// if res.notifier == nil { +// // create new notifier +// res.notifier = newNotifier() +// } - // check if version is selected - if res.SelectedVersion == nil { - res.selectVersion() - } +// // check if version is selected +// if res.SelectedVersion == nil { +// res.selectVersion() +// } - // create file - return &File{ - resource: res, - version: res.SelectedVersion, - notifier: res.notifier, - versionedPath: res.SelectedVersion.versionedPath(), - storagePath: res.SelectedVersion.storagePath(), - } -} +// // create file +// return &File{ +// resource: res, +// version: res.SelectedVersion, +// notifier: res.notifier, +// versionedPath: res.SelectedVersion.versionedPath(), +// storagePath: res.SelectedVersion.storagePath(), +// } +// } -//nolint:gocognit // function already kept as simple as possible -func (res *Resource) selectVersion() { - sort.Sort(res) +// //nolint:gocognit // function already kept as simple as possible +// func (res *Resource) selectVersion() { +// sort.Sort(res) - // export after we finish - var fallback bool - defer func() { - if fallback { - log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier) - } else { - log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier) - } +// // export after we finish +// var fallback bool +// defer func() { +// if fallback { +// log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier) +// } else { +// log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier) +// } - if res.inUse() && - res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version - res.notifier != nil { +// if res.inUse() && +// res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version +// res.notifier != nil { - res.notifier.markAsUpgradeable() - res.notifier = nil +// res.notifier.markAsUpgradeable() +// res.notifier = nil - log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber) - } - }() +// log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber) +// } +// }() - if len(res.Versions) == 0 { - // TODO: find better way to deal with an empty version slice (which should not happen) - res.SelectedVersion = nil - return - } +// if len(res.Versions) == 0 { +// // TODO: find better way to deal with an empty version slice (which should not happen) +// res.SelectedVersion = nil +// return +// } - // Target selection +// // Target selection - // 1) Dev release if dev mode is active and ignore blacklisting - if res.registry.DevMode { - // Get last version, as this will be v0.0.0, if available. - rv := res.Versions[len(res.Versions)-1] - // Check if it's v0.0.0. - if rv.semVer.Equal(devVersion) && rv.Available { - res.SelectedVersion = rv - return - } - } +// // 1) Dev release if dev mode is active and ignore blacklisting +// if res.registry.DevMode { +// // Get last version, as this will be v0.0.0, if available. +// rv := res.Versions[len(res.Versions)-1] +// // Check if it's v0.0.0. +// if rv.semVer.Equal(devVersion) && rv.Available { +// res.SelectedVersion = rv +// return +// } +// } - // 2) Find the current release. This may be also be a pre-release. - for _, rv := range res.Versions { - if rv.CurrentRelease { - if rv.isSelectable() { - res.SelectedVersion = rv - return - } - // There can only be once current release, - // so we can abort after finding one. - break - } - } +// // 2) Find the current release. This may be also be a pre-release. +// for _, rv := range res.Versions { +// if rv.CurrentRelease { +// if rv.isSelectable() { +// res.SelectedVersion = rv +// return +// } +// // There can only be once current release, +// // so we can abort after finding one. +// break +// } +// } - // 3) If UsePreReleases is set, find any newest version. - if res.registry.UsePreReleases { - for _, rv := range res.Versions { - if rv.isSelectable() { - res.SelectedVersion = rv - return - } - } - } +// // 3) If UsePreReleases is set, find any newest version. +// if res.registry.UsePreReleases { +// for _, rv := range res.Versions { +// if rv.isSelectable() { +// res.SelectedVersion = rv +// return +// } +// } +// } - // 4) Find the newest stable version. - for _, rv := range res.Versions { - if !rv.PreRelease && rv.isSelectable() { - res.SelectedVersion = rv - return - } - } +// // 4) Find the newest stable version. +// for _, rv := range res.Versions { +// if !rv.PreRelease && rv.isSelectable() { +// res.SelectedVersion = rv +// return +// } +// } - // 5) Default to newest. - res.SelectedVersion = res.Versions[0] - fallback = true -} +// // 5) Default to newest. +// res.SelectedVersion = res.Versions[0] +// fallback = true +// } -// Blacklist blacklists the specified version and selects a new version. -func (res *Resource) Blacklist(version string) error { - res.Lock() - defer res.Unlock() +// // Blacklist blacklists the specified version and selects a new version. +// func (res *Resource) Blacklist(version string) error { +// res.Lock() +// defer res.Unlock() - // count available and valid versions - valid := 0 - for _, rv := range res.Versions { - if rv.semVer.Equal(devVersion) { - continue // ignore dev versions - } - if !rv.Blacklisted { - valid++ - } - } - if valid <= 1 { - return errors.New("cannot blacklist last version") // last one, cannot blacklist! - } +// // count available and valid versions +// valid := 0 +// for _, rv := range res.Versions { +// if rv.semVer.Equal(devVersion) { +// continue // ignore dev versions +// } +// if !rv.Blacklisted { +// valid++ +// } +// } +// if valid <= 1 { +// return errors.New("cannot blacklist last version") // last one, cannot blacklist! +// } - // find version and blacklist - for _, rv := range res.Versions { - if rv.VersionNumber == version { - // blacklist and update - rv.Blacklisted = true - res.selectVersion() - return nil - } - } +// // find version and blacklist +// for _, rv := range res.Versions { +// if rv.VersionNumber == version { +// // blacklist and update +// rv.Blacklisted = true +// res.selectVersion() +// return nil +// } +// } - return errors.New("could not find version") -} +// return errors.New("could not find version") +// } -// Purge deletes old updates, retaining a certain amount, specified by -// the keep parameter. Purge will always keep at least 2 versions so -// specifying a smaller keep value will have no effect. -func (res *Resource) Purge(keepExtra int) { //nolint:gocognit - res.Lock() - defer res.Unlock() +// // Purge deletes old updates, retaining a certain amount, specified by +// // the keep parameter. Purge will always keep at least 2 versions so +// // specifying a smaller keep value will have no effect. +// func (res *Resource) Purge(keepExtra int) { //nolint:gocognit +// res.Lock() +// defer res.Unlock() - // If there is any blacklisted version within the resource, pause purging. - // In this case we may need extra available versions beyond what would be - // available after purging. - for _, rv := range res.Versions { - if rv.Blacklisted { - log.Debugf( - "%s: pausing purging of resource %s, as it contains blacklisted items", - res.registry.Name, - rv.resource.Identifier, - ) - return - } - } +// // If there is any blacklisted version within the resource, pause purging. +// // In this case we may need extra available versions beyond what would be +// // available after purging. +// for _, rv := range res.Versions { +// if rv.Blacklisted { +// log.Debugf( +// "%s: pausing purging of resource %s, as it contains blacklisted items", +// res.registry.Name, +// rv.resource.Identifier, +// ) +// return +// } +// } - // Safeguard the amount of extra version to keep. - if keepExtra < 2 { - keepExtra = 2 - } +// // Safeguard the amount of extra version to keep. +// if keepExtra < 2 { +// keepExtra = 2 +// } - // Search for purge boundary. - var purgeBoundary int - var skippedActiveVersion bool - var skippedSelectedVersion bool - var skippedStableVersion bool -boundarySearch: - for i, rv := range res.Versions { - // Check if required versions are already skipped. - switch { - case !skippedActiveVersion && res.ActiveVersion != nil: - // Skip versions until the active version, if it's set. - case !skippedSelectedVersion && res.SelectedVersion != nil: - // Skip versions until the selected version, if it's set. - case !skippedStableVersion: - // Skip versions until the stable version. - default: - // All required version skipped, set purge boundary. - purgeBoundary = i + keepExtra - break boundarySearch - } +// // Search for purge boundary. +// var purgeBoundary int +// var skippedActiveVersion bool +// var skippedSelectedVersion bool +// var skippedStableVersion bool +// boundarySearch: +// for i, rv := range res.Versions { +// // Check if required versions are already skipped. +// switch { +// case !skippedActiveVersion && res.ActiveVersion != nil: +// // Skip versions until the active version, if it's set. +// case !skippedSelectedVersion && res.SelectedVersion != nil: +// // Skip versions until the selected version, if it's set. +// case !skippedStableVersion: +// // Skip versions until the stable version. +// default: +// // All required version skipped, set purge boundary. +// purgeBoundary = i + keepExtra +// break boundarySearch +// } - // Check if current instance is a required version. - if rv == res.ActiveVersion { - skippedActiveVersion = true - } - if rv == res.SelectedVersion { - skippedSelectedVersion = true - } - if !rv.PreRelease { - skippedStableVersion = true - } - } +// // Check if current instance is a required version. +// if rv == res.ActiveVersion { +// skippedActiveVersion = true +// } +// if rv == res.SelectedVersion { +// skippedSelectedVersion = true +// } +// if !rv.PreRelease { +// skippedStableVersion = true +// } +// } - // Check if there is anything to purge at all. - if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) { - return - } +// // Check if there is anything to purge at all. +// if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) { +// return +// } - // Purge everything beyond the purge boundary. - for _, rv := range res.Versions[purgeBoundary:] { - // Only remove if resource file is actually available. - if !rv.Available { - continue - } +// // Purge everything beyond the purge boundary. +// for _, rv := range res.Versions[purgeBoundary:] { +// // Only remove if resource file is actually available. +// if !rv.Available { +// continue +// } - // Remove resource file. - storagePath := rv.storagePath() - err := os.Remove(storagePath) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) - } - } else { - log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) - } +// // Remove resource file. +// storagePath := rv.storagePath() +// err := os.Remove(storagePath) +// if err != nil { +// if !errors.Is(err, fs.ErrNotExist) { +// log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) +// } +// } else { +// log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) +// } - // Remove resource signature file. - err = os.Remove(rv.storageSigPath()) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) - } - } else { - log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) - } +// // Remove resource signature file. +// err = os.Remove(rv.storageSigPath()) +// if err != nil { +// if !errors.Is(err, fs.ErrNotExist) { +// log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) +// } +// } else { +// log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) +// } - // Remove unpacked version of resource. - ext := filepath.Ext(storagePath) - if ext == "" { - // Nothing to do if file does not have an extension. - continue - } - unpackedPath := strings.TrimSuffix(storagePath, ext) +// // Remove unpacked version of resource. +// ext := filepath.Ext(storagePath) +// if ext == "" { +// // Nothing to do if file does not have an extension. +// continue +// } +// unpackedPath := strings.TrimSuffix(storagePath, ext) - // Remove if it exists, or an error occurs on access. - _, err = os.Stat(unpackedPath) - if err == nil || !errors.Is(err, fs.ErrNotExist) { - err = os.Remove(unpackedPath) - if err != nil { - log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) - } else { - log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) - } - } - } +// // Remove if it exists, or an error occurs on access. +// _, err = os.Stat(unpackedPath) +// if err == nil || !errors.Is(err, fs.ErrNotExist) { +// err = os.Remove(unpackedPath) +// if err != nil { +// log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) +// } else { +// log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) +// } +// } +// } - // remove entries of deleted files - res.Versions = res.Versions[purgeBoundary:] -} +// // remove entries of deleted files +// res.Versions = res.Versions[purgeBoundary:] +// } -// SigningMetadata returns the metadata to be included in signatures. -func (rv *ResourceVersion) SigningMetadata() map[string]string { - return map[string]string{ - "id": rv.resource.Identifier, - "version": rv.VersionNumber, - } -} +// // SigningMetadata returns the metadata to be included in signatures. +// func (rv *ResourceVersion) SigningMetadata() map[string]string { +// return map[string]string{ +// "id": rv.resource.Identifier, +// "version": rv.VersionNumber, +// } +// } -// GetFile returns the version as a *File. -// It locks the resource for doing so. -func (rv *ResourceVersion) GetFile() *File { - rv.resource.Lock() - defer rv.resource.Unlock() +// // GetFile returns the version as a *File. +// // It locks the resource for doing so. +// func (rv *ResourceVersion) GetFile() *File { +// rv.resource.Lock() +// defer rv.resource.Unlock() - // check for notifier - if rv.resource.notifier == nil { - // create new notifier - rv.resource.notifier = newNotifier() - } +// // check for notifier +// if rv.resource.notifier == nil { +// // create new notifier +// rv.resource.notifier = newNotifier() +// } - // create file - return &File{ - resource: rv.resource, - version: rv, - notifier: rv.resource.notifier, - versionedPath: rv.versionedPath(), - storagePath: rv.storagePath(), - } -} +// // create file +// return &File{ +// resource: rv.resource, +// version: rv, +// notifier: rv.resource.notifier, +// versionedPath: rv.versionedPath(), +// storagePath: rv.storagePath(), +// } +// } -// versionedPath returns the versioned identifier. -func (rv *ResourceVersion) versionedPath() string { - return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) -} +// // versionedPath returns the versioned identifier. +// func (rv *ResourceVersion) versionedPath() string { +// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) +// } -// versionedSigPath returns the versioned identifier of the file signature. -func (rv *ResourceVersion) versionedSigPath() string { - return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension -} +// // versionedSigPath returns the versioned identifier of the file signature. +// func (rv *ResourceVersion) versionedSigPath() string { +// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension +// } -// storagePath returns the absolute storage path. -func (rv *ResourceVersion) storagePath() string { - return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath())) -} +// // storagePath returns the absolute storage path. +// func (rv *ResourceVersion) storagePath() string { +// return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath())) +// } -// storageSigPath returns the absolute storage path of the file signature. -func (rv *ResourceVersion) storageSigPath() string { - return rv.storagePath() + filesig.Extension -} +// // storageSigPath returns the absolute storage path of the file signature. +// func (rv *ResourceVersion) storageSigPath() string { +// return rv.storagePath() + filesig.Extension +// } diff --git a/base/updater/signing.go b/base/updater/signing.go index cffd5cbe..85f4eb6f 100644 --- a/base/updater/signing.go +++ b/base/updater/signing.go @@ -1,49 +1,49 @@ package updater -import ( - "strings" +// import ( +// "strings" - "github.com/safing/jess" -) +// "github.com/safing/jess" +// ) -// VerificationOptions holds options for verification of files. -type VerificationOptions struct { - TrustStore jess.TrustStore - DownloadPolicy SignaturePolicy - DiskLoadPolicy SignaturePolicy -} +// // VerificationOptions holds options for verification of files. +// type VerificationOptions struct { +// TrustStore jess.TrustStore +// DownloadPolicy SignaturePolicy +// DiskLoadPolicy SignaturePolicy +// } -// GetVerificationOptions returns the verification options for the given identifier. -func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { - if reg.Verification == nil { - return nil - } +// // GetVerificationOptions returns the verification options for the given identifier. +// func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { +// if reg.Verification == nil { +// return nil +// } - var ( - longestPrefix = -1 - bestMatch *VerificationOptions - ) - for prefix, opts := range reg.Verification { - if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { - longestPrefix = len(prefix) - bestMatch = opts - } - } +// var ( +// longestPrefix = -1 +// bestMatch *VerificationOptions +// ) +// for prefix, opts := range reg.Verification { +// if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { +// longestPrefix = len(prefix) +// bestMatch = opts +// } +// } - return bestMatch -} +// return bestMatch +// } -// SignaturePolicy defines behavior in case of errors. -type SignaturePolicy uint8 +// // SignaturePolicy defines behavior in case of errors. +// type SignaturePolicy uint8 -// Signature Policies. -const ( - // SignaturePolicyRequire fails on any error. - SignaturePolicyRequire = iota +// // Signature Policies. +// const ( +// // SignaturePolicyRequire fails on any error. +// SignaturePolicyRequire = iota - // SignaturePolicyWarn only warns on errors. - SignaturePolicyWarn +// // SignaturePolicyWarn only warns on errors. +// SignaturePolicyWarn - // SignaturePolicyDisable only downloads signatures, but does not verify them. - SignaturePolicyDisable -) +// // SignaturePolicyDisable only downloads signatures, but does not verify them. +// SignaturePolicyDisable +// ) diff --git a/base/updater/state.go b/base/updater/state.go index 20c27f46..8cbada6f 100644 --- a/base/updater/state.go +++ b/base/updater/state.go @@ -1,180 +1,180 @@ package updater -import ( - "sort" - "sync" - "time" +// import ( +// "sort" +// "sync" +// "time" - "github.com/safing/portmaster/base/utils" -) +// "github.com/safing/portmaster/base/utils" +// ) -// Registry States. -const ( - StateReady = "ready" // Default idle state. - StateChecking = "checking" // Downloading indexes. - StateDownloading = "downloading" // Downloading updates. - StateFetching = "fetching" // Fetching a single file. -) +// // Registry States. +// const ( +// StateReady = "ready" // Default idle state. +// StateChecking = "checking" // Downloading indexes. +// StateDownloading = "downloading" // Downloading updates. +// StateFetching = "fetching" // Fetching a single file. +// ) -// RegistryState describes the registry state. -type RegistryState struct { - sync.Mutex - reg *ResourceRegistry +// // RegistryState describes the registry state. +// type RegistryState struct { +// sync.Mutex +// reg *ResourceRegistry - // ID holds the ID of the state the registry is currently in. - ID string +// // ID holds the ID of the state the registry is currently in. +// ID string - // Details holds further information about the current state. - Details any +// // Details holds further information about the current state. +// Details any - // Updates holds generic information about the current status of pending - // and recently downloaded updates. - Updates UpdateState +// // Updates holds generic information about the current status of pending +// // and recently downloaded updates. +// Updates UpdateState - // operationLock locks the operation of any state changing operation. - // This is separate from the registry lock, which locks access to the - // registry struct. - operationLock sync.Mutex -} +// // operationLock locks the operation of any state changing operation. +// // This is separate from the registry lock, which locks access to the +// // registry struct. +// operationLock sync.Mutex +// } -// StateDownloadingDetails holds details of the downloading state. -type StateDownloadingDetails struct { - // Resources holds the resource IDs that are being downloaded. - Resources []string +// // StateDownloadingDetails holds details of the downloading state. +// type StateDownloadingDetails struct { +// // Resources holds the resource IDs that are being downloaded. +// Resources []string - // FinishedUpTo holds the index of Resources that is currently being - // downloaded. Previous resources have finished downloading. - FinishedUpTo int -} +// // FinishedUpTo holds the index of Resources that is currently being +// // downloaded. Previous resources have finished downloading. +// FinishedUpTo int +// } -// UpdateState holds generic information about the current status of pending -// and recently downloaded updates. -type UpdateState struct { - // LastCheckAt holds the time of the last update check. - LastCheckAt *time.Time - // LastCheckError holds the error of the last check. - LastCheckError error - // PendingDownload holds the resources that are pending download. - PendingDownload []string +// // UpdateState holds generic information about the current status of pending +// // and recently downloaded updates. +// type UpdateState struct { +// // LastCheckAt holds the time of the last update check. +// LastCheckAt *time.Time +// // LastCheckError holds the error of the last check. +// LastCheckError error +// // PendingDownload holds the resources that are pending download. +// PendingDownload []string - // LastDownloadAt holds the time when resources were downloaded the last time. - LastDownloadAt *time.Time - // LastDownloadError holds the error of the last download. - LastDownloadError error - // LastDownload holds the resources that we downloaded the last time updates - // were downloaded. - LastDownload []string +// // LastDownloadAt holds the time when resources were downloaded the last time. +// LastDownloadAt *time.Time +// // LastDownloadError holds the error of the last download. +// LastDownloadError error +// // LastDownload holds the resources that we downloaded the last time updates +// // were downloaded. +// LastDownload []string - // LastSuccessAt holds the time of the last successful update (check). - LastSuccessAt *time.Time -} +// // LastSuccessAt holds the time of the last successful update (check). +// LastSuccessAt *time.Time +// } -// GetState returns the current registry state. -// The returned data must not be modified. -func (reg *ResourceRegistry) GetState() RegistryState { - reg.state.Lock() - defer reg.state.Unlock() +// // GetState returns the current registry state. +// // The returned data must not be modified. +// func (reg *ResourceRegistry) GetState() RegistryState { +// reg.state.Lock() +// defer reg.state.Unlock() - return RegistryState{ - ID: reg.state.ID, - Details: reg.state.Details, - Updates: reg.state.Updates, - } -} +// return RegistryState{ +// ID: reg.state.ID, +// Details: reg.state.Details, +// Updates: reg.state.Updates, +// } +// } -// StartOperation starts an operation. -func (s *RegistryState) StartOperation(id string) bool { - defer s.notify() +// // StartOperation starts an operation. +// func (s *RegistryState) StartOperation(id string) bool { +// defer s.notify() - s.operationLock.Lock() +// s.operationLock.Lock() - s.Lock() - defer s.Unlock() +// s.Lock() +// defer s.Unlock() - s.ID = id - return true -} +// s.ID = id +// return true +// } -// UpdateOperationDetails updates the details of an operation. -// The supplied struct should be a copy and must not be changed after calling -// this function. -func (s *RegistryState) UpdateOperationDetails(details any) { - defer s.notify() +// // UpdateOperationDetails updates the details of an operation. +// // The supplied struct should be a copy and must not be changed after calling +// // this function. +// func (s *RegistryState) UpdateOperationDetails(details any) { +// defer s.notify() - s.Lock() - defer s.Unlock() +// s.Lock() +// defer s.Unlock() - s.Details = details -} +// s.Details = details +// } -// EndOperation ends an operation. -func (s *RegistryState) EndOperation() { - defer s.notify() - defer s.operationLock.Unlock() +// // EndOperation ends an operation. +// func (s *RegistryState) EndOperation() { +// defer s.notify() +// defer s.operationLock.Unlock() - s.Lock() - defer s.Unlock() +// s.Lock() +// defer s.Unlock() - s.ID = StateReady - s.Details = nil -} +// s.ID = StateReady +// s.Details = nil +// } -// ReportUpdateCheck reports an update check to the registry state. -func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { - defer s.notify() +// // ReportUpdateCheck reports an update check to the registry state. +// func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { +// defer s.notify() - sort.Strings(pendingDownload) +// sort.Strings(pendingDownload) - s.Lock() - defer s.Unlock() +// s.Lock() +// defer s.Unlock() - now := time.Now() - s.Updates.LastCheckAt = &now - s.Updates.LastCheckError = failed - s.Updates.PendingDownload = pendingDownload +// now := time.Now() +// s.Updates.LastCheckAt = &now +// s.Updates.LastCheckError = failed +// s.Updates.PendingDownload = pendingDownload - if failed == nil { - s.Updates.LastSuccessAt = &now - } -} +// if failed == nil { +// s.Updates.LastSuccessAt = &now +// } +// } -// ReportDownloads reports downloaded updates to the registry state. -func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { - defer s.notify() +// // ReportDownloads reports downloaded updates to the registry state. +// func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { +// defer s.notify() - sort.Strings(downloaded) +// sort.Strings(downloaded) - s.Lock() - defer s.Unlock() +// s.Lock() +// defer s.Unlock() - now := time.Now() - s.Updates.LastDownloadAt = &now - s.Updates.LastDownloadError = failed - s.Updates.LastDownload = downloaded +// now := time.Now() +// s.Updates.LastDownloadAt = &now +// s.Updates.LastDownloadError = failed +// s.Updates.LastDownload = downloaded - // Remove downloaded resources from the pending list. - if len(s.Updates.PendingDownload) > 0 { - newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) - for _, pending := range s.Updates.PendingDownload { - if !utils.StringInSlice(downloaded, pending) { - newPendingDownload = append(newPendingDownload, pending) - } - } - s.Updates.PendingDownload = newPendingDownload - } +// // Remove downloaded resources from the pending list. +// if len(s.Updates.PendingDownload) > 0 { +// newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) +// for _, pending := range s.Updates.PendingDownload { +// if !utils.StringInSlice(downloaded, pending) { +// newPendingDownload = append(newPendingDownload, pending) +// } +// } +// s.Updates.PendingDownload = newPendingDownload +// } - if failed == nil { - s.Updates.LastSuccessAt = &now - } -} +// if failed == nil { +// s.Updates.LastSuccessAt = &now +// } +// } -func (s *RegistryState) notify() { - switch { - case s.reg == nil: - return - case s.reg.StateNotifyFunc == nil: - return - } +// func (s *RegistryState) notify() { +// switch { +// case s.reg == nil: +// return +// case s.reg.StateNotifyFunc == nil: +// return +// } - s.reg.StateNotifyFunc(s) -} +// s.reg.StateNotifyFunc(s) +// } diff --git a/base/updater/storage.go b/base/updater/storage.go index cd05bdbd..8cfe8444 100644 --- a/base/updater/storage.go +++ b/base/updater/storage.go @@ -1,272 +1,272 @@ package updater -import ( - "context" - "errors" - "fmt" - "io/fs" - "net/http" - "os" - "path/filepath" - "strings" +// import ( +// "context" +// "errors" +// "fmt" +// "io/fs" +// "net/http" +// "os" +// "path/filepath" +// "strings" - "github.com/safing/jess/filesig" - "github.com/safing/jess/lhash" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) +// "github.com/safing/jess/filesig" +// "github.com/safing/jess/lhash" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) -// ScanStorage scans root within the storage dir and adds found -// resources to the registry. If an error occurred, it is logged -// and the last error is returned. Everything that was found -// despite errors is added to the registry anyway. Leave root -// empty to scan the full storage dir. -func (reg *ResourceRegistry) ScanStorage(root string) error { - var lastError error +// // ScanStorage scans root within the storage dir and adds found +// // resources to the registry. If an error occurred, it is logged +// // and the last error is returned. Everything that was found +// // despite errors is added to the registry anyway. Leave root +// // empty to scan the full storage dir. +// func (reg *ResourceRegistry) ScanStorage(root string) error { +// var lastError error - // prep root - if root == "" { - root = reg.storageDir.Path - } else { - var err error - root, err = filepath.Abs(root) - if err != nil { - return err - } - if !strings.HasPrefix(root, reg.storageDir.Path) { - return errors.New("supplied scan root path not within storage") - } - } +// // prep root +// if root == "" { +// root = reg.storageDir.Path +// } else { +// var err error +// root, err = filepath.Abs(root) +// if err != nil { +// return err +// } +// if !strings.HasPrefix(root, reg.storageDir.Path) { +// return errors.New("supplied scan root path not within storage") +// } +// } - // walk fs - _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - // skip tmp dir (including errors trying to read it) - if strings.HasPrefix(path, reg.tmpDir.Path) { - return filepath.SkipDir - } +// // walk fs +// _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { +// // skip tmp dir (including errors trying to read it) +// if strings.HasPrefix(path, reg.tmpDir.Path) { +// return filepath.SkipDir +// } - // handle walker error - if err != nil { - lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) - log.Warning(lastError.Error()) - return nil - } +// // handle walker error +// if err != nil { +// lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) +// log.Warning(lastError.Error()) +// return nil +// } - // Ignore file signatures. - if strings.HasSuffix(path, filesig.Extension) { - return nil - } +// // Ignore file signatures. +// if strings.HasSuffix(path, filesig.Extension) { +// return nil +// } - // get relative path to storage - relativePath, err := filepath.Rel(reg.storageDir.Path, path) - if err != nil { - lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) - log.Warning(lastError.Error()) - return nil - } +// // get relative path to storage +// relativePath, err := filepath.Rel(reg.storageDir.Path, path) +// if err != nil { +// lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) +// log.Warning(lastError.Error()) +// return nil +// } - // convert to identifier and version - relativePath = filepath.ToSlash(relativePath) - identifier, version, ok := GetIdentifierAndVersion(relativePath) - if !ok { - // file does not conform to format - return nil - } +// // convert to identifier and version +// relativePath = filepath.ToSlash(relativePath) +// identifier, version, ok := GetIdentifierAndVersion(relativePath) +// if !ok { +// // file does not conform to format +// return nil +// } - // fully ignore directories that also have an identifier - these will be unpacked resources - if info.IsDir() { - return filepath.SkipDir - } +// // fully ignore directories that also have an identifier - these will be unpacked resources +// if info.IsDir() { +// return filepath.SkipDir +// } - // save - err = reg.AddResource(identifier, version, nil, true, false, false) - if err != nil { - lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) - log.Warning(lastError.Error()) - } - return nil - }) +// // save +// err = reg.AddResource(identifier, version, nil, true, false, false) +// if err != nil { +// lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) +// log.Warning(lastError.Error()) +// } +// return nil +// }) - return lastError -} +// return lastError +// } -// LoadIndexes loads the current release indexes from disk -// or will fetch a new version if not available and the -// registry is marked as online. -func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { - var firstErr error - client := &http.Client{} - for _, idx := range reg.getIndexes() { - err := reg.loadIndexFile(idx) - if err == nil { - log.Debugf("%s: loaded index %s", reg.Name, idx.Path) - } else if reg.Online { - // try to download the index file if a local disk version - // does not exist or we don't have permission to read it. - if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { - err = reg.downloadIndex(ctx, client, idx) - } - } +// // LoadIndexes loads the current release indexes from disk +// // or will fetch a new version if not available and the +// // registry is marked as online. +// func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { +// var firstErr error +// client := &http.Client{} +// for _, idx := range reg.getIndexes() { +// err := reg.loadIndexFile(idx) +// if err == nil { +// log.Debugf("%s: loaded index %s", reg.Name, idx.Path) +// } else if reg.Online { +// // try to download the index file if a local disk version +// // does not exist or we don't have permission to read it. +// if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { +// err = reg.downloadIndex(ctx, client, idx) +// } +// } - if err != nil && firstErr == nil { - firstErr = err - } - } +// if err != nil && firstErr == nil { +// firstErr = err +// } +// } - return firstErr -} +// return firstErr +// } -// getIndexes returns a copy of the index. -// The indexes itself are references. -func (reg *ResourceRegistry) getIndexes() []*Index { - reg.RLock() - defer reg.RUnlock() +// // getIndexes returns a copy of the index. +// // The indexes itself are references. +// func (reg *ResourceRegistry) getIndexes() []*Index { +// reg.RLock() +// defer reg.RUnlock() - indexes := make([]*Index, len(reg.indexes)) - copy(indexes, reg.indexes) - return indexes -} +// indexes := make([]*Index, len(reg.indexes)) +// copy(indexes, reg.indexes) +// return indexes +// } -func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { - indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) - indexData, err := os.ReadFile(indexPath) - if err != nil { - return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) - } +// func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { +// indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) +// indexData, err := os.ReadFile(indexPath) +// if err != nil { +// return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) +// } - // Verify signature, if enabled. - if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { - // Load and check signature. - verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) - if err != nil { - switch verifOpts.DiskLoadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) - case SignaturePolicyWarn: - log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) - case SignaturePolicyDisable: - log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) - } - } +// // Verify signature, if enabled. +// if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { +// // Load and check signature. +// verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) +// if err != nil { +// switch verifOpts.DiskLoadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) +// case SignaturePolicyWarn: +// log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) +// case SignaturePolicyDisable: +// log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) +// } +// } - // Check if signature checksum matches the index data. - if err == nil && !verifiedHash.Matches(indexData) { - switch verifOpts.DiskLoadPolicy { - case SignaturePolicyRequire: - return fmt.Errorf("index file %s does not match signature", idx.Path) - case SignaturePolicyWarn: - log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) - case SignaturePolicyDisable: - log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) - } - } - } +// // Check if signature checksum matches the index data. +// if err == nil && !verifiedHash.Matches(indexData) { +// switch verifOpts.DiskLoadPolicy { +// case SignaturePolicyRequire: +// return fmt.Errorf("index file %s does not match signature", idx.Path) +// case SignaturePolicyWarn: +// log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) +// case SignaturePolicyDisable: +// log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) +// } +// } +// } - // Parse the index file. - indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) - if err != nil { - return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) - } +// // Parse the index file. +// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) +// if err != nil { +// return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) +// } - // Update last seen release. - idx.LastRelease = indexFile.Published +// // Update last seen release. +// idx.LastRelease = indexFile.Published - // Warn if there aren't any releases in the index. - if len(indexFile.Releases) == 0 { - log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) - return nil - } +// // Warn if there aren't any releases in the index. +// if len(indexFile.Releases) == 0 { +// log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) +// return nil +// } - // Add index releases to available resources. - err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) - if err != nil { - log.Warningf("%s: failed to add resource: %s", reg.Name, err) - } - return nil -} +// // Add index releases to available resources. +// err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) +// if err != nil { +// log.Warningf("%s: failed to add resource: %s", reg.Name, err) +// } +// return nil +// } -func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { - // Load signature file. - sigFileData, err := os.ReadFile(sigFilePath) - if err != nil { - return nil, nil, fmt.Errorf("failed to read signature file: %w", err) - } +// func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { +// // Load signature file. +// sigFileData, err := os.ReadFile(sigFilePath) +// if err != nil { +// return nil, nil, fmt.Errorf("failed to read signature file: %w", err) +// } - // Extract all signatures. - sigs, err := filesig.ParseSigFile(sigFileData) - switch { - case len(sigs) == 0 && err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - case len(sigs) == 0: - return nil, nil, errors.New("no signatures found in signature file") - case err != nil: - return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) - } +// // Extract all signatures. +// sigs, err := filesig.ParseSigFile(sigFileData) +// switch { +// case len(sigs) == 0 && err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// case len(sigs) == 0: +// return nil, nil, errors.New("no signatures found in signature file") +// case err != nil: +// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) +// } - // Verify all signatures. - var verifiedHash *lhash.LabeledHash - for _, sig := range sigs { - fd, err := filesig.VerifyFileData( - sig, - nil, - verifOpts.TrustStore, - ) - if err != nil { - return nil, sigFileData, err - } +// // Verify all signatures. +// var verifiedHash *lhash.LabeledHash +// for _, sig := range sigs { +// fd, err := filesig.VerifyFileData( +// sig, +// nil, +// verifOpts.TrustStore, +// ) +// if err != nil { +// return nil, sigFileData, err +// } - // Save or check verified hash. - if verifiedHash == nil { - verifiedHash = fd.FileHash() - } else if !fd.FileHash().Equal(verifiedHash) { - // Return an error if two valid hashes mismatch. - // For simplicity, all hash algorithms must be the same for now. - return nil, sigFileData, errors.New("file hashes from different signatures do not match") - } - } +// // Save or check verified hash. +// if verifiedHash == nil { +// verifiedHash = fd.FileHash() +// } else if !fd.FileHash().Equal(verifiedHash) { +// // Return an error if two valid hashes mismatch. +// // For simplicity, all hash algorithms must be the same for now. +// return nil, sigFileData, errors.New("file hashes from different signatures do not match") +// } +// } - return verifiedHash, sigFileData, nil -} +// return verifiedHash, sigFileData, nil +// } -// CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. -func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { - err := os.RemoveAll(symlinkRoot.Path) - if err != nil { - return fmt.Errorf("failed to wipe symlink root: %w", err) - } +// // CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. +// func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { +// err := os.RemoveAll(symlinkRoot.Path) +// if err != nil { +// return fmt.Errorf("failed to wipe symlink root: %w", err) +// } - err = symlinkRoot.Ensure() - if err != nil { - return fmt.Errorf("failed to create symlink root: %w", err) - } +// err = symlinkRoot.Ensure() +// if err != nil { +// return fmt.Errorf("failed to create symlink root: %w", err) +// } - reg.RLock() - defer reg.RUnlock() +// reg.RLock() +// defer reg.RUnlock() - for _, res := range reg.resources { - if res.SelectedVersion == nil { - return fmt.Errorf("no selected version available for %s", res.Identifier) - } +// for _, res := range reg.resources { +// if res.SelectedVersion == nil { +// return fmt.Errorf("no selected version available for %s", res.Identifier) +// } - targetPath := res.SelectedVersion.storagePath() - linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) - linkPathDir := filepath.Dir(linkPath) +// targetPath := res.SelectedVersion.storagePath() +// linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) +// linkPathDir := filepath.Dir(linkPath) - err = symlinkRoot.EnsureAbsPath(linkPathDir) - if err != nil { - return fmt.Errorf("failed to create dir for link: %w", err) - } +// err = symlinkRoot.EnsureAbsPath(linkPathDir) +// if err != nil { +// return fmt.Errorf("failed to create dir for link: %w", err) +// } - relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) - if err != nil { - return fmt.Errorf("failed to get relative target path: %w", err) - } +// relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) +// if err != nil { +// return fmt.Errorf("failed to get relative target path: %w", err) +// } - err = os.Symlink(relativeTargetPath, linkPath) - if err != nil { - return fmt.Errorf("failed to link %s: %w", res.Identifier, err) - } - } +// err = os.Symlink(relativeTargetPath, linkPath) +// if err != nil { +// return fmt.Errorf("failed to link %s: %w", res.Identifier, err) +// } +// } - return nil -} +// return nil +// } diff --git a/base/updater/unpacking.go b/base/updater/unpacking.go index 75d48921..5b157695 100644 --- a/base/updater/unpacking.go +++ b/base/updater/unpacking.go @@ -1,195 +1,195 @@ package updater -import ( - "archive/zip" - "compress/gzip" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "path/filepath" - "strings" +// import ( +// "archive/zip" +// "compress/gzip" +// "errors" +// "fmt" +// "io" +// "io/fs" +// "os" +// "path" +// "path/filepath" +// "strings" - "github.com/hashicorp/go-multierror" +// "github.com/hashicorp/go-multierror" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) -// MaxUnpackSize specifies the maximum size that will be unpacked. -const MaxUnpackSize = 1000000000 // 1GB +// // MaxUnpackSize specifies the maximum size that will be unpacked. +// const MaxUnpackSize = 1000000000 // 1GB -// UnpackGZIP unpacks a GZIP compressed reader r -// and returns a new reader. It's suitable to be -// used with registry.GetPackedFile. -func UnpackGZIP(r io.Reader) (io.Reader, error) { - return gzip.NewReader(r) -} +// // UnpackGZIP unpacks a GZIP compressed reader r +// // and returns a new reader. It's suitable to be +// // used with registry.GetPackedFile. +// func UnpackGZIP(r io.Reader) (io.Reader, error) { +// return gzip.NewReader(r) +// } -// UnpackResources unpacks all resources defined in the AutoUnpack list. -func (reg *ResourceRegistry) UnpackResources() error { - reg.RLock() - defer reg.RUnlock() +// // UnpackResources unpacks all resources defined in the AutoUnpack list. +// func (reg *ResourceRegistry) UnpackResources() error { +// reg.RLock() +// defer reg.RUnlock() - var multierr *multierror.Error - for _, res := range reg.resources { - if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { - err := res.UnpackArchive() - if err != nil { - multierr = multierror.Append( - multierr, - fmt.Errorf("%s: %w", res.Identifier, err), - ) - } - } - } +// var multierr *multierror.Error +// for _, res := range reg.resources { +// if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { +// err := res.UnpackArchive() +// if err != nil { +// multierr = multierror.Append( +// multierr, +// fmt.Errorf("%s: %w", res.Identifier, err), +// ) +// } +// } +// } - return multierr.ErrorOrNil() -} +// return multierr.ErrorOrNil() +// } -const ( - zipSuffix = ".zip" -) +// const ( +// zipSuffix = ".zip" +// ) -// UnpackArchive unpacks the archive the resource refers to. The contents are -// unpacked into a directory with the same name as the file, excluding the -// suffix. If the destination folder already exists, it is assumed that the -// contents have already been correctly unpacked. -func (res *Resource) UnpackArchive() error { - res.Lock() - defer res.Unlock() +// // UnpackArchive unpacks the archive the resource refers to. The contents are +// // unpacked into a directory with the same name as the file, excluding the +// // suffix. If the destination folder already exists, it is assumed that the +// // contents have already been correctly unpacked. +// func (res *Resource) UnpackArchive() error { +// res.Lock() +// defer res.Unlock() - // Only unpack selected versions. - if res.SelectedVersion == nil { - return nil - } +// // Only unpack selected versions. +// if res.SelectedVersion == nil { +// return nil +// } - switch { - case strings.HasSuffix(res.Identifier, zipSuffix): - return res.unpackZipArchive() - default: - return fmt.Errorf("unsupported file type for unpacking") - } -} +// switch { +// case strings.HasSuffix(res.Identifier, zipSuffix): +// return res.unpackZipArchive() +// default: +// return fmt.Errorf("unsupported file type for unpacking") +// } +// } -func (res *Resource) unpackZipArchive() error { - // Get file and directory paths. - archiveFile := res.SelectedVersion.storagePath() - destDir := strings.TrimSuffix(archiveFile, zipSuffix) - tmpDir := filepath.Join( - res.registry.tmpDir.Path, - filepath.FromSlash(strings.TrimSuffix( - path.Base(res.SelectedVersion.versionedPath()), - zipSuffix, - )), - ) +// func (res *Resource) unpackZipArchive() error { +// // Get file and directory paths. +// archiveFile := res.SelectedVersion.storagePath() +// destDir := strings.TrimSuffix(archiveFile, zipSuffix) +// tmpDir := filepath.Join( +// res.registry.tmpDir.Path, +// filepath.FromSlash(strings.TrimSuffix( +// path.Base(res.SelectedVersion.versionedPath()), +// zipSuffix, +// )), +// ) - // Check status of destination. - dstStat, err := os.Stat(destDir) - switch { - case errors.Is(err, fs.ErrNotExist): - // The destination does not exist, continue with unpacking. - case err != nil: - return fmt.Errorf("cannot access destination for unpacking: %w", err) - case !dstStat.IsDir(): - return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) - default: - // Archive already seems to be unpacked. - return nil - } +// // Check status of destination. +// dstStat, err := os.Stat(destDir) +// switch { +// case errors.Is(err, fs.ErrNotExist): +// // The destination does not exist, continue with unpacking. +// case err != nil: +// return fmt.Errorf("cannot access destination for unpacking: %w", err) +// case !dstStat.IsDir(): +// return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) +// default: +// // Archive already seems to be unpacked. +// return nil +// } - // Create the tmp directory for unpacking. - err = res.registry.tmpDir.EnsureAbsPath(tmpDir) - if err != nil { - return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) - } +// // Create the tmp directory for unpacking. +// err = res.registry.tmpDir.EnsureAbsPath(tmpDir) +// if err != nil { +// return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) +// } - // Defer clean up of directories. - defer func() { - // Always clean up the tmp dir. - _ = os.RemoveAll(tmpDir) - // Cleanup the destination in case of an error. - if err != nil { - _ = os.RemoveAll(destDir) - } - }() +// // Defer clean up of directories. +// defer func() { +// // Always clean up the tmp dir. +// _ = os.RemoveAll(tmpDir) +// // Cleanup the destination in case of an error. +// if err != nil { +// _ = os.RemoveAll(destDir) +// } +// }() - // Open the archive for reading. - var archiveReader *zip.ReadCloser - archiveReader, err = zip.OpenReader(archiveFile) - if err != nil { - return fmt.Errorf("failed to open zip reader: %w", err) - } - defer func() { - _ = archiveReader.Close() - }() +// // Open the archive for reading. +// var archiveReader *zip.ReadCloser +// archiveReader, err = zip.OpenReader(archiveFile) +// if err != nil { +// return fmt.Errorf("failed to open zip reader: %w", err) +// } +// defer func() { +// _ = archiveReader.Close() +// }() - // Save all files to the tmp dir. - for _, file := range archiveReader.File { - err = copyFromZipArchive( - file, - filepath.Join(tmpDir, filepath.FromSlash(file.Name)), - ) - if err != nil { - return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) - } - } +// // Save all files to the tmp dir. +// for _, file := range archiveReader.File { +// err = copyFromZipArchive( +// file, +// filepath.Join(tmpDir, filepath.FromSlash(file.Name)), +// ) +// if err != nil { +// return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) +// } +// } - // Make the final move. - err = os.Rename(tmpDir, destDir) - if err != nil { - return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) - } +// // Make the final move. +// err = os.Rename(tmpDir, destDir) +// if err != nil { +// return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) +// } - // Fix permissions on the destination dir. - err = res.registry.storageDir.EnsureAbsPath(destDir) - if err != nil { - return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) - } +// // Fix permissions on the destination dir. +// err = res.registry.storageDir.EnsureAbsPath(destDir) +// if err != nil { +// return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) +// } - log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) - return nil -} +// log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) +// return nil +// } -func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { - // If file is a directory, create it and continue. - if archiveFile.FileInfo().IsDir() { - err := os.Mkdir(dstPath, archiveFile.Mode()) - if err != nil { - return fmt.Errorf("failed to create directory %s: %w", dstPath, err) - } - return nil - } +// func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { +// // If file is a directory, create it and continue. +// if archiveFile.FileInfo().IsDir() { +// err := os.Mkdir(dstPath, archiveFile.Mode()) +// if err != nil { +// return fmt.Errorf("failed to create directory %s: %w", dstPath, err) +// } +// return nil +// } - // Open archived file for reading. - fileReader, err := archiveFile.Open() - if err != nil { - return fmt.Errorf("failed to open file in archive: %w", err) - } - defer func() { - _ = fileReader.Close() - }() +// // Open archived file for reading. +// fileReader, err := archiveFile.Open() +// if err != nil { +// return fmt.Errorf("failed to open file in archive: %w", err) +// } +// defer func() { +// _ = fileReader.Close() +// }() - // Open destination file for writing. - dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) - if err != nil { - return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) - } - defer func() { - _ = dstFile.Close() - }() +// // Open destination file for writing. +// dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) +// if err != nil { +// return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) +// } +// defer func() { +// _ = dstFile.Close() +// }() - // Copy full file from archive to dst. - if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { - // EOF is expected here as the archive is likely smaller - // thane MaxUnpackSize - if errors.Is(err, io.EOF) { - return nil - } - return err - } +// // Copy full file from archive to dst. +// if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { +// // EOF is expected here as the archive is likely smaller +// // thane MaxUnpackSize +// if errors.Is(err, io.EOF) { +// return nil +// } +// return err +// } - return nil -} +// return nil +// } diff --git a/base/updater/updating.go b/base/updater/updating.go index cf87472e..3c76304a 100644 --- a/base/updater/updating.go +++ b/base/updater/updating.go @@ -1,359 +1,359 @@ package updater -import ( - "context" - "fmt" - "net/http" - "os" - "path" - "path/filepath" - "strings" +// import ( +// "context" +// "fmt" +// "net/http" +// "os" +// "path" +// "path/filepath" +// "strings" - "golang.org/x/exp/slices" +// "golang.org/x/exp/slices" - "github.com/safing/jess/filesig" - "github.com/safing/jess/lhash" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" -) +// "github.com/safing/jess/filesig" +// "github.com/safing/jess/lhash" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" +// ) -// UpdateIndexes downloads all indexes. An error is only returned when all -// indexes fail to update. -func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { - var lastErr error - var anySuccess bool +// // UpdateIndexes downloads all indexes. An error is only returned when all +// // indexes fail to update. +// func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { +// var lastErr error +// var anySuccess bool - // Start registry operation. - reg.state.StartOperation(StateChecking) - defer reg.state.EndOperation() +// // Start registry operation. +// reg.state.StartOperation(StateChecking) +// defer reg.state.EndOperation() - client := &http.Client{} - for _, idx := range reg.getIndexes() { - if err := reg.downloadIndex(ctx, client, idx); err != nil { - lastErr = err - log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) - } else { - anySuccess = true - } - } +// client := &http.Client{} +// for _, idx := range reg.getIndexes() { +// if err := reg.downloadIndex(ctx, client, idx); err != nil { +// lastErr = err +// log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) +// } else { +// anySuccess = true +// } +// } - // If all indexes failed to update, fail. - if !anySuccess { - err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) - reg.state.ReportUpdateCheck(nil, err) - return err - } +// // If all indexes failed to update, fail. +// if !anySuccess { +// err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) +// reg.state.ReportUpdateCheck(nil, err) +// return err +// } - // Get pending resources and update status. - pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) - reg.state.ReportUpdateCheck( - humanInfoFromResourceVersions(pendingResourceVersions), - nil, - ) +// // Get pending resources and update status. +// pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) +// reg.state.ReportUpdateCheck( +// humanInfoFromResourceVersions(pendingResourceVersions), +// nil, +// ) - return nil -} +// return nil +// } -func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { - var ( - // Index. - indexErr error - indexData []byte - downloadURL string +// func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { +// var ( +// // Index. +// indexErr error +// indexData []byte +// downloadURL string - // Signature. - sigErr error - verifiedHash *lhash.LabeledHash - sigFileData []byte - verifOpts = reg.GetVerificationOptions(idx.Path) - ) +// // Signature. +// sigErr error +// verifiedHash *lhash.LabeledHash +// sigFileData []byte +// verifOpts = reg.GetVerificationOptions(idx.Path) +// ) - // Upgrade to v2 index if verification is enabled. - downloadIndexPath := idx.Path - if verifOpts != nil { - downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension - } +// // Upgrade to v2 index if verification is enabled. +// downloadIndexPath := idx.Path +// if verifOpts != nil { +// downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension +// } - // Download new index and signature. - for tries := range 3 { - // Index and signature need to be fetched together, so that they are - // fetched from the same source. One source should always have a matching - // index and signature. Backup sources may be behind a little. - // If the signature verification fails, another source should be tried. +// // Download new index and signature. +// for tries := range 3 { +// // Index and signature need to be fetched together, so that they are +// // fetched from the same source. One source should always have a matching +// // index and signature. Backup sources may be behind a little. +// // If the signature verification fails, another source should be tried. - // Get index data. - indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) - if indexErr != nil { - log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) - continue - } +// // Get index data. +// indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) +// if indexErr != nil { +// log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) +// continue +// } - // Get signature and verify it. - if verifOpts != nil { - verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( - ctx, client, - verifOpts, downloadIndexPath+filesig.Extension, nil, - tries, - ) - if sigErr != nil { - log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) - continue - } +// // Get signature and verify it. +// if verifOpts != nil { +// verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( +// ctx, client, +// verifOpts, downloadIndexPath+filesig.Extension, nil, +// tries, +// ) +// if sigErr != nil { +// log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) +// continue +// } - // Check if the index matches the verified hash. - if verifiedHash.Matches(indexData) { - log.Infof("%s: verified signature of %s", reg.Name, downloadURL) - } else { - sigErr = ErrIndexChecksumMismatch - log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) - continue - } - } +// // Check if the index matches the verified hash. +// if verifiedHash.Matches(indexData) { +// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) +// } else { +// sigErr = ErrIndexChecksumMismatch +// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) +// continue +// } +// } - break - } - if indexErr != nil { - return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) - } - if sigErr != nil { - return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) - } +// break +// } +// if indexErr != nil { +// return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) +// } +// if sigErr != nil { +// return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) +// } - // Parse the index file. - indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) - if err != nil { - return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) - } +// // Parse the index file. +// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) +// if err != nil { +// return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) +// } - // Add index data to registry. - if len(indexFile.Releases) > 0 { - // Check if all resources are within the indexes' authority. - authoritativePath := path.Dir(idx.Path) + "/" - if authoritativePath == "./" { - // Fix path for indexes at the storage root. - authoritativePath = "" - } - cleanedData := make(map[string]string, len(indexFile.Releases)) - for key, version := range indexFile.Releases { - if strings.HasPrefix(key, authoritativePath) { - cleanedData[key] = version - } else { - log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) - } - } +// // Add index data to registry. +// if len(indexFile.Releases) > 0 { +// // Check if all resources are within the indexes' authority. +// authoritativePath := path.Dir(idx.Path) + "/" +// if authoritativePath == "./" { +// // Fix path for indexes at the storage root. +// authoritativePath = "" +// } +// cleanedData := make(map[string]string, len(indexFile.Releases)) +// for key, version := range indexFile.Releases { +// if strings.HasPrefix(key, authoritativePath) { +// cleanedData[key] = version +// } else { +// log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) +// } +// } - // add resources to registry - err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) - if err != nil { - log.Warningf("%s: failed to add resources: %s", reg.Name, err) - } - } else { - log.Debugf("%s: index %s is empty", reg.Name, idx.Path) - } +// // add resources to registry +// err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) +// if err != nil { +// log.Warningf("%s: failed to add resources: %s", reg.Name, err) +// } +// } else { +// log.Debugf("%s: index %s is empty", reg.Name, idx.Path) +// } - // Check if dest dir exists. - indexDir := filepath.FromSlash(path.Dir(idx.Path)) - err = reg.storageDir.EnsureRelPath(indexDir) - if err != nil { - log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) - } +// // Check if dest dir exists. +// indexDir := filepath.FromSlash(path.Dir(idx.Path)) +// err = reg.storageDir.EnsureRelPath(indexDir) +// if err != nil { +// log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) +// } - // Index files must be readable by portmaster-staert with user permissions in order to load the index. - err = os.WriteFile( //nolint:gosec - filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), - indexData, 0o0644, - ) - if err != nil { - log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) - } +// // Index files must be readable by portmaster-staert with user permissions in order to load the index. +// err = os.WriteFile( //nolint:gosec +// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), +// indexData, 0o0644, +// ) +// if err != nil { +// log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) +// } - // Write signature file, if we have one. - if len(sigFileData) > 0 { - err = os.WriteFile( //nolint:gosec - filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), - sigFileData, 0o0644, - ) - if err != nil { - log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) - } - } +// // Write signature file, if we have one. +// if len(sigFileData) > 0 { +// err = os.WriteFile( //nolint:gosec +// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), +// sigFileData, 0o0644, +// ) +// if err != nil { +// log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) +// } +// } - log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) - return nil -} +// log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) +// return nil +// } -// DownloadUpdates checks if updates are available and downloads updates of used components. -func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { - // Start registry operation. - reg.state.StartOperation(StateDownloading) - defer reg.state.EndOperation() +// // DownloadUpdates checks if updates are available and downloads updates of used components. +// func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { +// // Start registry operation. +// reg.state.StartOperation(StateDownloading) +// defer reg.state.EndOperation() - // Get pending updates. - toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) - downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) - reg.state.UpdateOperationDetails(&StateDownloadingDetails{ - Resources: downloadDetailsResources, - }) +// // Get pending updates. +// toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) +// downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) +// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ +// Resources: downloadDetailsResources, +// }) - // nothing to update - if len(toUpdate) == 0 && len(missingSigs) == 0 { - log.Infof("%s: everything up to date", reg.Name) - return nil - } +// // nothing to update +// if len(toUpdate) == 0 && len(missingSigs) == 0 { +// log.Infof("%s: everything up to date", reg.Name) +// return nil +// } - // check download dir - if err := reg.tmpDir.Ensure(); err != nil { - return fmt.Errorf("could not prepare tmp directory for download: %w", err) - } +// // check download dir +// if err := reg.tmpDir.Ensure(); err != nil { +// return fmt.Errorf("could not prepare tmp directory for download: %w", err) +// } - // download updates - log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) - client := &http.Client{} - var reportError error +// // download updates +// log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) +// client := &http.Client{} +// var reportError error - for i, rv := range toUpdate { - log.Infof( - "%s: downloading update [%d/%d]: %s version %s", - reg.Name, - i+1, len(toUpdate), - rv.resource.Identifier, rv.VersionNumber, - ) - var err error - for tries := range 3 { - err = reg.fetchFile(ctx, client, rv, tries) - if err == nil { - // Update resource version state. - rv.resource.Lock() - rv.Available = true - if rv.resource.VerificationOptions != nil { - rv.SigAvailable = true - } - rv.resource.Unlock() +// for i, rv := range toUpdate { +// log.Infof( +// "%s: downloading update [%d/%d]: %s version %s", +// reg.Name, +// i+1, len(toUpdate), +// rv.resource.Identifier, rv.VersionNumber, +// ) +// var err error +// for tries := range 3 { +// err = reg.fetchFile(ctx, client, rv, tries) +// if err == nil { +// // Update resource version state. +// rv.resource.Lock() +// rv.Available = true +// if rv.resource.VerificationOptions != nil { +// rv.SigAvailable = true +// } +// rv.resource.Unlock() - break - } - } - if err != nil { - reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) - log.Warningf("%s: %s", reg.Name, reportError) - } +// break +// } +// } +// if err != nil { +// reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) +// log.Warningf("%s: %s", reg.Name, reportError) +// } - reg.state.UpdateOperationDetails(&StateDownloadingDetails{ - Resources: downloadDetailsResources, - FinishedUpTo: i + 1, - }) - } +// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ +// Resources: downloadDetailsResources, +// FinishedUpTo: i + 1, +// }) +// } - if len(missingSigs) > 0 { - log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) +// if len(missingSigs) > 0 { +// log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) - for _, rv := range missingSigs { - var err error - for tries := range 3 { - err = reg.fetchMissingSig(ctx, client, rv, tries) - if err == nil { - // Update resource version state. - rv.resource.Lock() - rv.SigAvailable = true - rv.resource.Unlock() +// for _, rv := range missingSigs { +// var err error +// for tries := range 3 { +// err = reg.fetchMissingSig(ctx, client, rv, tries) +// if err == nil { +// // Update resource version state. +// rv.resource.Lock() +// rv.SigAvailable = true +// rv.resource.Unlock() - break - } - } - if err != nil { - reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) - log.Warningf("%s: %s", reg.Name, reportError) - } - } - } +// break +// } +// } +// if err != nil { +// reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) +// log.Warningf("%s: %s", reg.Name, reportError) +// } +// } +// } - reg.state.ReportDownloads( - downloadDetailsResources, - reportError, - ) - log.Infof("%s: finished downloading updates", reg.Name) +// reg.state.ReportDownloads( +// downloadDetailsResources, +// reportError, +// ) +// log.Infof("%s: finished downloading updates", reg.Name) - return nil -} +// return nil +// } -// DownloadUpdates checks if updates are available and downloads updates of used components. +// // DownloadUpdates checks if updates are available and downloads updates of used components. -// GetPendingDownloads returns the list of pending downloads. -// If manual is set, indexes with AutoDownload=false will be checked. -// If auto is set, indexes with AutoDownload=true will be checked. -func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { - reg.RLock() - defer reg.RUnlock() +// // GetPendingDownloads returns the list of pending downloads. +// // If manual is set, indexes with AutoDownload=false will be checked. +// // If auto is set, indexes with AutoDownload=true will be checked. +// func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { +// reg.RLock() +// defer reg.RUnlock() - // create list of downloads - var toUpdate []*ResourceVersion - var missingSigs []*ResourceVersion +// // create list of downloads +// var toUpdate []*ResourceVersion +// var missingSigs []*ResourceVersion - for _, res := range reg.resources { - func() { - res.Lock() - defer res.Unlock() +// for _, res := range reg.resources { +// func() { +// res.Lock() +// defer res.Unlock() - // Skip resources without index or indexes that should not be reported - // according to parameters. - switch { - case res.Index == nil: - // Cannot download if resource is not part of an index. - return - case manual && !res.Index.AutoDownload: - // Manual update report and index is not auto-download. - case auto && res.Index.AutoDownload: - // Auto update report and index is auto-download. - default: - // Resource should not be reported. - return - } +// // Skip resources without index or indexes that should not be reported +// // according to parameters. +// switch { +// case res.Index == nil: +// // Cannot download if resource is not part of an index. +// return +// case manual && !res.Index.AutoDownload: +// // Manual update report and index is not auto-download. +// case auto && res.Index.AutoDownload: +// // Auto update report and index is auto-download. +// default: +// // Resource should not be reported. +// return +// } - // Skip resources we don't need. - switch { - case res.inUse(): - // Update if resource is in use. - case res.available(): - // Update if resource is available locally, ie. was used in the past. - case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): - // Update is set as mandatory. - default: - // Resource does not need to be updated. - return - } +// // Skip resources we don't need. +// switch { +// case res.inUse(): +// // Update if resource is in use. +// case res.available(): +// // Update if resource is available locally, ie. was used in the past. +// case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): +// // Update is set as mandatory. +// default: +// // Resource does not need to be updated. +// return +// } - // Go through all versions until we find versions that need updating. - for _, rv := range res.Versions { - switch { - case !rv.CurrentRelease: - // We are not interested in older releases. - case !rv.Available: - // File not available locally, download! - toUpdate = append(toUpdate, rv) - case !rv.SigAvailable && res.VerificationOptions != nil: - // File signature is not available and verification is enabled, download signature! - missingSigs = append(missingSigs, rv) - } - } - }() - } +// // Go through all versions until we find versions that need updating. +// for _, rv := range res.Versions { +// switch { +// case !rv.CurrentRelease: +// // We are not interested in older releases. +// case !rv.Available: +// // File not available locally, download! +// toUpdate = append(toUpdate, rv) +// case !rv.SigAvailable && res.VerificationOptions != nil: +// // File signature is not available and verification is enabled, download signature! +// missingSigs = append(missingSigs, rv) +// } +// } +// }() +// } - slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { - return strings.Compare(a.resource.Identifier, b.resource.Identifier) - }) - slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { - return strings.Compare(a.resource.Identifier, b.resource.Identifier) - }) +// slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { +// return strings.Compare(a.resource.Identifier, b.resource.Identifier) +// }) +// slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { +// return strings.Compare(a.resource.Identifier, b.resource.Identifier) +// }) - return toUpdate, missingSigs -} +// return toUpdate, missingSigs +// } -func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { - identifiers := make([]string, len(resourceVersions)) +// func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { +// identifiers := make([]string, len(resourceVersions)) - for i, rv := range resourceVersions { - identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) - } +// for i, rv := range resourceVersions { +// identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) +// } - return identifiers -} +// return identifiers +// } diff --git a/service/broadcasts/data.go b/service/broadcasts/data.go index 2b59e4e6..e92ce84f 100644 --- a/service/broadcasts/data.go +++ b/service/broadcasts/data.go @@ -7,7 +7,6 @@ import ( "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/service/intel/geoip" "github.com/safing/portmaster/service/netenv" - "github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/spn/access" "github.com/safing/portmaster/spn/access/account" "github.com/safing/portmaster/spn/captain" @@ -18,18 +17,19 @@ var portmasterStarted = time.Now() func collectData() interface{} { data := make(map[string]interface{}) + // TODO(vladimir) // Get data about versions. - versions := updates.GetSimpleVersions() - data["Updates"] = versions - data["Version"] = versions.Build.Version - numericVersion, err := MakeNumericVersion(versions.Build.Version) - if err != nil { - data["NumericVersion"] = &DataError{ - Error: err, - } - } else { - data["NumericVersion"] = numericVersion - } + // versions := updates.GetSimpleVersions() + // data["Updates"] = versions + // data["Version"] = versions.Build.Version + // numericVersion, err := MakeNumericVersion(versions.Build.Version) + // if err != nil { + // data["NumericVersion"] = &DataError{ + // Error: err, + // } + // } else { + // data["NumericVersion"] = numericVersion + // } // Get data about install. installInfo, err := GetInstallInfo() diff --git a/service/broadcasts/module.go b/service/broadcasts/module.go index a3968933..2d99115b 100644 --- a/service/broadcasts/module.go +++ b/service/broadcasts/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) type Broadcasts struct { @@ -91,4 +92,6 @@ func New(instance instance) (*Broadcasts, error) { return module, nil } -type instance interface{} +type instance interface { + Updates() *updates.Updates +} diff --git a/service/broadcasts/notify.go b/service/broadcasts/notify.go index a010f249..73b05f98 100644 --- a/service/broadcasts/notify.go +++ b/service/broadcasts/notify.go @@ -18,7 +18,6 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" ) const ( @@ -68,7 +67,7 @@ type BroadcastNotification struct { func broadcastNotify(ctx *mgr.WorkerCtx) error { // Get broadcast notifications file, load it from disk and parse it. - broadcastsResource, err := updates.GetFile(broadcastsResourcePath) + broadcastsResource, err := module.instance.Updates().GetFile(broadcastsResourcePath) if err != nil { return fmt.Errorf("failed to get broadcast notifications update: %w", err) } diff --git a/service/core/api.go b/service/core/api.go index c4758cda..c633956e 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -149,7 +149,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) { config.AddToDebugInfo(di) // Detailed information. - updates.AddToDebugInfo(di) + // TODO(vladimir): updates.AddToDebugInfo(di) compat.AddToDebugInfo(di) module.instance.AddWorkerInfoToDebugInfo(di) di.AddGoroutineStack() diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go index 5f55323c..d6b5b757 100644 --- a/service/intel/filterlists/database.go +++ b/service/intel/filterlists/database.go @@ -14,15 +14,14 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" ) const ( - baseListFilePath = "intel/lists/base.dsdl" - intermediateListFilePath = "intel/lists/intermediate.dsdl" - urgentListFilePath = "intel/lists/urgent.dsdl" - listIndexFilePath = "intel/lists/index.dsd" + baseListFilePath = "base.dsdl" + intermediateListFilePath = "intermediate.dsdl" + urgentListFilePath = "urgent.dsdl" + listIndexFilePath = "index.dsd" ) // default bloomfilter element sizes (estimated). @@ -40,9 +39,9 @@ var ( filterListLock sync.RWMutex // Updater files for tracking upgrades. - baseFile *updater.File - intermediateFile *updater.File - urgentFile *updater.File + baseFile *registry.File + intermediateFile *registry.File + urgentFile *registry.File filterListsLoaded chan struct{} ) @@ -56,11 +55,10 @@ var cache = database.NewInterface(&database.Options{ // getFileFunc is the function used to get a file from // the updater. It's basically updates.GetFile and used // for unit testing. -type getFileFunc func(string) (*updater.File, error) // getFile points to updates.GetFile but may be set to // something different during unit testing. -var getFile getFileFunc = updates.GetFile +// var getFile getFileFunc = registry.GetFile func init() { filterListsLoaded = make(chan struct{}) @@ -79,7 +77,7 @@ func isLoaded() bool { // processListFile opens the latest version of file and decodes it's DSDL // content. It calls processEntry for each decoded filterlists entry. -func processListFile(ctx context.Context, filter *scopedBloom, file *updater.File) error { +func processListFile(ctx context.Context, filter *scopedBloom, file *registry.File) error { f, err := os.Open(file.Path()) if err != nil { return err diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index 4b59adde..842a96b2 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -4,14 +4,12 @@ import ( "errors" "fmt" "os" - "strings" "sync" "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" "github.com/safing/structures/dsd" ) @@ -164,7 +162,7 @@ func getListIndexFromCache() (*ListIndexFile, error) { var ( // listIndexUpdate must only be used by updateListIndex. - listIndexUpdate *updater.File + listIndexUpdate *registry.File listIndexUpdateLock sync.Mutex ) @@ -177,24 +175,24 @@ func updateListIndex() error { case listIndexUpdate == nil: // This is the first time this function is run, get updater file for index. var err error - listIndexUpdate, err = updates.GetFile(listIndexFilePath) + listIndexUpdate, err = module.instance.Updates().GetFile(listIndexFilePath) if err != nil { return err } // Check if the version in the cache is current. - index, err := getListIndexFromCache() + _, err = getListIndexFromCache() switch { case errors.Is(err, database.ErrNotFound): log.Info("filterlists: index not in cache, starting update") case err != nil: log.Warningf("filterlists: failed to load index from cache, starting update: %s", err) - case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")): - log.Infof( - "filterlists: index from cache is outdated, starting update (%s != %s)", - strings.TrimPrefix(index.Version, "v"), - listIndexUpdate.Version(), - ) + // case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")): + // log.Infof( + // "filterlists: index from cache is outdated, starting update (%s != %s)", + // strings.TrimPrefix(index.Version, "v"), + // listIndexUpdate.Version(), + // ) default: // List is in cache and current, there is nothing to do. log.Debug("filterlists: index is up to date") @@ -204,8 +202,8 @@ func updateListIndex() error { return nil } - case listIndexUpdate.UpgradeAvailable(): - log.Info("filterlists: index update available, starting update") + // case listIndexUpdate.UpgradeAvailable(): + // log.Info("filterlists: index update available, starting update") default: // Index is loaded and no update is available, there is nothing to do. return nil diff --git a/service/intel/filterlists/updater.go b/service/intel/filterlists/updater.go index 72f7b82e..f2e5f8d2 100644 --- a/service/intel/filterlists/updater.go +++ b/service/intel/filterlists/updater.go @@ -13,8 +13,8 @@ import ( "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/query" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates/registry" ) var updateInProgress = abool.New() @@ -174,51 +174,51 @@ func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error { // getUpgradableFiles returns a slice of filterlists files // that should be updated. The files MUST be updated and // processed in the returned order! -func getUpgradableFiles() ([]*updater.File, error) { - var updateOrder []*updater.File +func getUpgradableFiles() ([]*registry.File, error) { + var updateOrder []*registry.File - cacheDBInUse := isLoaded() + // cacheDBInUse := isLoaded() - if baseFile == nil || baseFile.UpgradeAvailable() || !cacheDBInUse { - var err error - baseFile, err = getFile(baseListFilePath) - if err != nil { - return nil, err - } - log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version()) - updateOrder = append(updateOrder, baseFile) - } + // if baseFile == nil || !cacheDBInUse { // TODO(vladimir): || baseFile.UpgradeAvailable() + // var err error + // baseFile, err = module.instance.Updates().GetFile(baseListFilePath) + // if err != nil { + // return nil, err + // } + // log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version()) + // updateOrder = append(updateOrder, baseFile) + // } - if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse { - var err error - intermediateFile, err = getFile(intermediateListFilePath) - if err != nil && !errors.Is(err, updater.ErrNotFound) { - return nil, err - } + // if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse { + // var err error + // intermediateFile, err = getFile(intermediateListFilePath) + // if err != nil && !errors.Is(err, updater.ErrNotFound) { + // return nil, err + // } - if err == nil { - log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version()) - updateOrder = append(updateOrder, intermediateFile) - } - } + // if err == nil { + // log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version()) + // updateOrder = append(updateOrder, intermediateFile) + // } + // } - if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse { - var err error - urgentFile, err = getFile(urgentListFilePath) - if err != nil && !errors.Is(err, updater.ErrNotFound) { - return nil, err - } + // if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse { + // var err error + // urgentFile, err = getFile(urgentListFilePath) + // if err != nil && !errors.Is(err, updater.ErrNotFound) { + // return nil, err + // } - if err == nil { - log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version()) - updateOrder = append(updateOrder, urgentFile) - } - } + // if err == nil { + // log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version()) + // updateOrder = append(updateOrder, urgentFile) + // } + // } return resolveUpdateOrder(updateOrder) } -func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) { +func resolveUpdateOrder(updateOrder []*registry.File) ([]*registry.File, error) { // sort the update order by ascending version sort.Sort(byAscVersion(updateOrder)) log.Tracef("intel/filterlists: order of updates: %v", updateOrder) @@ -258,7 +258,7 @@ func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) { return updateOrder[startAtIdx:], nil } -type byAscVersion []*updater.File +type byAscVersion []*registry.File func (fs byAscVersion) Len() int { return len(fs) } diff --git a/service/intel/geoip/database.go b/service/intel/geoip/database.go index 5f0258a7..72197cd8 100644 --- a/service/intel/geoip/database.go +++ b/service/intel/geoip/database.go @@ -8,9 +8,8 @@ import ( maxminddb "github.com/oschwald/maxminddb-golang" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" ) var worker *updateWorker @@ -22,13 +21,13 @@ func init() { } const ( - v4MMDBResource = "intel/geoip/geoipv4.mmdb.gz" - v6MMDBResource = "intel/geoip/geoipv6.mmdb.gz" + v4MMDBResource = "geoipv4.mmdb" + v6MMDBResource = "geoipv6.mmdb" ) type geoIPDB struct { *maxminddb.Reader - file *updater.File + file *registry.File } // updateBroadcaster stores a geoIPDB and provides synchronized @@ -47,7 +46,7 @@ func (ub *updateBroadcaster) NeedsUpdate() bool { ub.rw.RLock() defer ub.rw.RUnlock() - return ub.db == nil || ub.db.file.UpgradeAvailable() + return ub.db == nil // TODO(vladimir) is this needed: || ub.db.file.UpgradeAvailable() } // ReplaceDatabase replaces (or initially sets) the mmdb database. @@ -181,12 +180,12 @@ func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error { func getGeoIPDB(resource string) (*geoIPDB, error) { log.Debugf("geoip: opening database %s", resource) - file, unpackedPath, err := openAndUnpack(resource) + file, err := open(resource) if err != nil { return nil, err } - reader, err := maxminddb.Open(unpackedPath) + reader, err := maxminddb.Open(file.Path()) if err != nil { return nil, fmt.Errorf("failed to open: %w", err) } @@ -198,16 +197,16 @@ func getGeoIPDB(resource string) (*geoIPDB, error) { }, nil } -func openAndUnpack(resource string) (*updater.File, string, error) { - f, err := updates.GetFile(resource) +func open(resource string) (*registry.File, error) { + f, err := module.instance.Updates().GetFile(resource) if err != nil { - return nil, "", fmt.Errorf("getting file: %w", err) + return nil, fmt.Errorf("getting file: %w", err) } - unpacked, err := f.Unpack(".gz", updater.UnpackGZIP) - if err != nil { - return nil, "", fmt.Errorf("unpacking file: %w", err) - } + // unpacked, err := f.Unpack(".gz", updater.UnpackGZIP) + // if err != nil { + // return nil, "", fmt.Errorf("unpacking file: %w", err) + // } - return f, unpacked, nil + return f, nil } diff --git a/service/netenv/main.go b/service/netenv/main.go index e1a68150..20426772 100644 --- a/service/netenv/main.go +++ b/service/netenv/main.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) // Event Names. @@ -105,4 +106,6 @@ func New(instance instance) (*NetEnv, error) { return module, nil } -type instance interface{} +type instance interface { + Updates() *updates.Updates +} diff --git a/service/netenv/online-status.go b/service/netenv/online-status.go index 554fc004..137ce410 100644 --- a/service/netenv/online-status.go +++ b/service/netenv/online-status.go @@ -17,7 +17,6 @@ import ( "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network/netutils" - "github.com/safing/portmaster/service/updates" ) // OnlineStatus represent a state of connectivity to the Internet. @@ -221,7 +220,7 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) // Trigger update check when coming (semi) online. if Online() { - _ = updates.TriggerUpdate(false, false) + module.instance.Updates().EventResourcesUpdated.Submit(struct{}{}) } } } diff --git a/service/network/api.go b/service/network/api.go index 82b11ad0..8af6eb26 100644 --- a/service/network/api.go +++ b/service/network/api.go @@ -16,7 +16,6 @@ import ( "github.com/safing/portmaster/service/process" "github.com/safing/portmaster/service/resolver" "github.com/safing/portmaster/service/status" - "github.com/safing/portmaster/service/updates" ) func registerAPIEndpoints() error { @@ -94,7 +93,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) { config.AddToDebugInfo(di) // Detailed information. - updates.AddToDebugInfo(di) + // TODO(vladimir): updates.AddToDebugInfo(di) // compat.AddToDebugInfo(di) // TODO: Cannot use due to interception import requirement which we don't want for SPN Hubs. di.AddGoroutineStack() diff --git a/service/ui/module.go b/service/ui/module.go index 630808e5..e9a8b324 100644 --- a/service/ui/module.go +++ b/service/ui/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) func prep() error { @@ -56,7 +57,10 @@ func (ui *UI) Stop() error { return nil } -var shimLoaded atomic.Bool +var ( + shimLoaded atomic.Bool + module *UI +) // New returns a new UI module. func New(instance instance) (*UI, error) { @@ -64,7 +68,7 @@ func New(instance instance) (*UI, error) { return nil, errors.New("only one instance allowed") } m := mgr.New("UI") - module := &UI{ + module = &UI{ mgr: m, instance: instance, } @@ -78,4 +82,5 @@ func New(instance instance) (*UI, error) { type instance interface { API() *api.API + Updates() *updates.Updates } diff --git a/service/ui/serve.go b/service/ui/serve.go index 9dca6c30..1455c3f6 100644 --- a/service/ui/serve.go +++ b/service/ui/serve.go @@ -15,9 +15,8 @@ import ( "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/base/utils" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" ) var ( @@ -92,9 +91,9 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // get file from update system - zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName)) + zipFile, err := module.instance.Updates().GetFile(fmt.Sprintf("%s.zip", moduleName)) if err != nil { - if errors.Is(err, updater.ErrNotFound) { + if errors.Is(err, registry.ErrNotFound) { log.Tracef("ui: requested module %s does not exist", moduleName) http.Error(w, err.Error(), http.StatusNotFound) } else { diff --git a/service/updates/api.go b/service/updates/api.go index 88659620..6c4dbf0c 100644 --- a/service/updates/api.go +++ b/service/updates/api.go @@ -1,161 +1,161 @@ package updates import ( - "bytes" - "io" - "net/http" - "os" - "path/filepath" - "strings" +// "bytes" +// "io" +// "net/http" +// "os" +// "path/filepath" +// "strings" - "github.com/ghodss/yaml" +// "github.com/ghodss/yaml" - "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" +// "github.com/safing/portmaster/base/api" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/utils" ) const ( apiPathCheckForUpdates = "updates/check" ) -func registerAPIEndpoints() error { - if err := api.RegisterEndpoint(api.Endpoint{ - Name: "Check for Updates", - Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", - Parameters: []api.Parameter{{ - Method: http.MethodPost, - Field: "download", - Value: "", - Description: "Force downloading and applying of all updates, regardless of auto-update settings.", - }}, - Path: apiPathCheckForUpdates, - Write: api.PermitUser, - ActionFunc: func(r *api.Request) (msg string, err error) { - // Check if we should also download regardless of settings. - downloadAll := r.URL.Query().Has("download") +// func registerAPIEndpoints() error { +// if err := api.RegisterEndpoint(api.Endpoint{ +// Name: "Check for Updates", +// Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", +// Parameters: []api.Parameter{{ +// Method: http.MethodPost, +// Field: "download", +// Value: "", +// Description: "Force downloading and applying of all updates, regardless of auto-update settings.", +// }}, +// Path: apiPathCheckForUpdates, +// Write: api.PermitUser, +// ActionFunc: func(r *api.Request) (msg string, err error) { +// // Check if we should also download regardless of settings. +// downloadAll := r.URL.Query().Has("download") - // Trigger update task. - err = TriggerUpdate(true, downloadAll) - if err != nil { - return "", err - } +// // Trigger update task. +// err = TriggerUpdate(true, downloadAll) +// if err != nil { +// return "", err +// } - // Report how we triggered. - if downloadAll { - return "downloading all updates...", nil - } - return "checking for updates...", nil - }, - }); err != nil { - return err - } +// // Report how we triggered. +// if downloadAll { +// return "downloading all updates...", nil +// } +// return "checking for updates...", nil +// }, +// }); err != nil { +// return err +// } - if err := api.RegisterEndpoint(api.Endpoint{ - Name: "Get Resource", - Description: "Returns the requested resource from the udpate system", - Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, - Read: api.PermitUser, - ReadMethod: http.MethodGet, - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - // Get identifier from URL. - var identifier string - if ar := api.GetAPIRequest(r); ar != nil { - identifier = ar.URLVars["identifier"] - } - if identifier == "" { - http.Error(w, "no resource speicified", http.StatusBadRequest) - return - } +// if err := api.RegisterEndpoint(api.Endpoint{ +// Name: "Get Resource", +// Description: "Returns the requested resource from the udpate system", +// Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, +// Read: api.PermitUser, +// ReadMethod: http.MethodGet, +// HandlerFunc: func(w http.ResponseWriter, r *http.Request) { +// // Get identifier from URL. +// var identifier string +// if ar := api.GetAPIRequest(r); ar != nil { +// identifier = ar.URLVars["identifier"] +// } +// if identifier == "" { +// http.Error(w, "no resource speicified", http.StatusBadRequest) +// return +// } - // Get resource. - resource, err := registry.GetFile(identifier) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } +// // Get resource. +// resource, err := registry.GetFile(identifier) +// if err != nil { +// http.Error(w, err.Error(), http.StatusNotFound) +// return +// } - // Open file for reading. - file, err := os.Open(resource.Path()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() //nolint:errcheck,gosec +// // Open file for reading. +// file, err := os.Open(resource.Path()) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// defer file.Close() //nolint:errcheck,gosec - // Assign file to reader - var reader io.Reader = file +// // Assign file to reader +// var reader io.Reader = file - // Add version to header. - w.Header().Set("Resource-Version", resource.Version()) +// // Add version to header. +// w.Header().Set("Resource-Version", resource.Version()) - // Set Content-Type. - contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) - w.Header().Set("Content-Type", contentType) +// // Set Content-Type. +// contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) +// w.Header().Set("Content-Type", contentType) - // Check if the content type may be returned. - accept := r.Header.Get("Accept") - if accept != "" { - mimeTypes := strings.Split(accept, ",") - // First, clean mime types. - for i, mimeType := range mimeTypes { - mimeType = strings.TrimSpace(mimeType) - mimeType, _, _ = strings.Cut(mimeType, ";") - mimeTypes[i] = mimeType - } - // Second, check if we may return anything. - var acceptsAny bool - for _, mimeType := range mimeTypes { - switch mimeType { - case "*", "*/*": - acceptsAny = true - } - } - // Third, check if we can convert. - if !acceptsAny { - var converted bool - sourceType, _, _ := strings.Cut(contentType, ";") - findConvertiblePair: - for _, mimeType := range mimeTypes { - switch { - case sourceType == "application/yaml" && mimeType == "application/json": - yamlData, err := io.ReadAll(reader) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - jsonData, err := yaml.YAMLToJSON(yamlData) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - reader = bytes.NewReader(jsonData) - converted = true - break findConvertiblePair - } - } +// // Check if the content type may be returned. +// accept := r.Header.Get("Accept") +// if accept != "" { +// mimeTypes := strings.Split(accept, ",") +// // First, clean mime types. +// for i, mimeType := range mimeTypes { +// mimeType = strings.TrimSpace(mimeType) +// mimeType, _, _ = strings.Cut(mimeType, ";") +// mimeTypes[i] = mimeType +// } +// // Second, check if we may return anything. +// var acceptsAny bool +// for _, mimeType := range mimeTypes { +// switch mimeType { +// case "*", "*/*": +// acceptsAny = true +// } +// } +// // Third, check if we can convert. +// if !acceptsAny { +// var converted bool +// sourceType, _, _ := strings.Cut(contentType, ";") +// findConvertiblePair: +// for _, mimeType := range mimeTypes { +// switch { +// case sourceType == "application/yaml" && mimeType == "application/json": +// yamlData, err := io.ReadAll(reader) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// jsonData, err := yaml.YAMLToJSON(yamlData) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// reader = bytes.NewReader(jsonData) +// converted = true +// break findConvertiblePair +// } +// } - // If we could not convert to acceptable format, return an error. - if !converted { - http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) - return - } - } - } +// // If we could not convert to acceptable format, return an error. +// if !converted { +// http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) +// return +// } +// } +// } - // Write file. - w.WriteHeader(http.StatusOK) - if r.Method != http.MethodHead { - _, err = io.Copy(w, reader) - if err != nil { - log.Errorf("updates: failed to serve resource file: %s", err) - return - } - } - }, - }); err != nil { - return err - } +// // Write file. +// w.WriteHeader(http.StatusOK) +// if r.Method != http.MethodHead { +// _, err = io.Copy(w, reader) +// if err != nil { +// log.Errorf("updates: failed to serve resource file: %s", err) +// return +// } +// } +// }, +// }); err != nil { +// return err +// } - return nil -} +// return nil +// } diff --git a/service/updates/config.go b/service/updates/config.go index f765fd4c..e563d557 100644 --- a/service/updates/config.go +++ b/service/updates/config.go @@ -4,9 +4,9 @@ import ( "github.com/tevino/abool" "github.com/safing/portmaster/base/config" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" + // "github.com/safing/portmaster/base/log" + // "github.com/safing/portmaster/service/mgr" + // "github.com/safing/portmaster/service/updates/helper" ) const cfgDevModeKey = "core/devMode" @@ -27,152 +27,152 @@ var ( forceDownload = abool.New() ) -func registerConfig() error { - err := config.Register(&config.Option{ - Name: "Release Channel", - Key: helper.ReleaseChannelKey, - Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, - OptType: config.OptTypeString, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - RequiresRestart: true, - DefaultValue: helper.ReleaseChannelStable, - PossibleValues: []config.PossibleValue{ - { - Name: "Stable", - Description: "Production releases.", - Value: helper.ReleaseChannelStable, - }, - { - Name: "Beta", - Description: "Production releases for testing new features that may break and cause interruption.", - Value: helper.ReleaseChannelBeta, - }, - { - Name: "Support", - Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", - Value: helper.ReleaseChannelSupport, - }, - { - Name: "Staging", - Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", - Value: helper.ReleaseChannelStaging, - }, - }, - Annotations: config.Annotations{ - config.DisplayOrderAnnotation: -4, - config.DisplayHintAnnotation: config.DisplayHintOneOf, - config.CategoryAnnotation: "Updates", - }, - }) - if err != nil { - return err - } +// func registerConfig() error { +// err := config.Register(&config.Option{ +// Name: "Release Channel", +// Key: helper.ReleaseChannelKey, +// Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, +// OptType: config.OptTypeString, +// ExpertiseLevel: config.ExpertiseLevelExpert, +// ReleaseLevel: config.ReleaseLevelStable, +// RequiresRestart: true, +// DefaultValue: helper.ReleaseChannelStable, +// PossibleValues: []config.PossibleValue{ +// { +// Name: "Stable", +// Description: "Production releases.", +// Value: helper.ReleaseChannelStable, +// }, +// { +// Name: "Beta", +// Description: "Production releases for testing new features that may break and cause interruption.", +// Value: helper.ReleaseChannelBeta, +// }, +// { +// Name: "Support", +// Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", +// Value: helper.ReleaseChannelSupport, +// }, +// { +// Name: "Staging", +// Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", +// Value: helper.ReleaseChannelStaging, +// }, +// }, +// Annotations: config.Annotations{ +// config.DisplayOrderAnnotation: -4, +// config.DisplayHintAnnotation: config.DisplayHintOneOf, +// config.CategoryAnnotation: "Updates", +// }, +// }) +// if err != nil { +// return err +// } - err = config.Register(&config.Option{ - Name: "Automatic Software Updates", - Key: enableSoftwareUpdatesKey, - Description: "Automatically check for and download software updates. This does not include intelligence data updates.", - OptType: config.OptTypeBool, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - RequiresRestart: false, - DefaultValue: true, - Annotations: config.Annotations{ - config.DisplayOrderAnnotation: -12, - config.CategoryAnnotation: "Updates", - }, - }) - if err != nil { - return err - } +// err = config.Register(&config.Option{ +// Name: "Automatic Software Updates", +// Key: enableSoftwareUpdatesKey, +// Description: "Automatically check for and download software updates. This does not include intelligence data updates.", +// OptType: config.OptTypeBool, +// ExpertiseLevel: config.ExpertiseLevelExpert, +// ReleaseLevel: config.ReleaseLevelStable, +// RequiresRestart: false, +// DefaultValue: true, +// Annotations: config.Annotations{ +// config.DisplayOrderAnnotation: -12, +// config.CategoryAnnotation: "Updates", +// }, +// }) +// if err != nil { +// return err +// } - err = config.Register(&config.Option{ - Name: "Automatic Intelligence Data Updates", - Key: enableIntelUpdatesKey, - Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", - OptType: config.OptTypeBool, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - RequiresRestart: false, - DefaultValue: true, - Annotations: config.Annotations{ - config.DisplayOrderAnnotation: -11, - config.CategoryAnnotation: "Updates", - }, - }) - if err != nil { - return err - } +// err = config.Register(&config.Option{ +// Name: "Automatic Intelligence Data Updates", +// Key: enableIntelUpdatesKey, +// Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", +// OptType: config.OptTypeBool, +// ExpertiseLevel: config.ExpertiseLevelExpert, +// ReleaseLevel: config.ReleaseLevelStable, +// RequiresRestart: false, +// DefaultValue: true, +// Annotations: config.Annotations{ +// config.DisplayOrderAnnotation: -11, +// config.CategoryAnnotation: "Updates", +// }, +// }) +// if err != nil { +// return err +// } - return nil -} +// return nil +// } -func initConfig() { - releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) - initialReleaseChannel = releaseChannel() - previousReleaseChannel = releaseChannel() +// func initConfig() { +// releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) +// initialReleaseChannel = releaseChannel() +// previousReleaseChannel = releaseChannel() - enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) - enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) - softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() - intelUpdatesCurrentlyEnabled = enableIntelUpdates() +// enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) +// enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) +// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() +// intelUpdatesCurrentlyEnabled = enableIntelUpdates() - devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false) - previousDevMode = devMode() -} +// devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false) +// previousDevMode = devMode() +// } -func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { - changed := false +// func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { +// changed := false - if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { - softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() - changed = true - } +// if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { +// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() +// changed = true +// } - if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { - intelUpdatesCurrentlyEnabled = enableIntelUpdates() - changed = true - } +// if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { +// intelUpdatesCurrentlyEnabled = enableIntelUpdates() +// changed = true +// } - if devMode() != previousDevMode { - registry.SetDevMode(devMode()) - previousDevMode = devMode() - changed = true - } +// if devMode() != previousDevMode { +// registry.SetDevMode(devMode()) +// previousDevMode = devMode() +// changed = true +// } - if releaseChannel() != previousReleaseChannel { - previousReleaseChannel = releaseChannel() - changed = true - } +// if releaseChannel() != previousReleaseChannel { +// previousReleaseChannel = releaseChannel() +// changed = true +// } - if changed { - // Update indexes based on new settings. - warning := helper.SetIndexes( - registry, - releaseChannel(), - true, - softwareUpdatesCurrentlyEnabled, - intelUpdatesCurrentlyEnabled, - ) - if warning != nil { - log.Warningf("updates: %s", warning) - } +// if changed { +// // Update indexes based on new settings. +// warning := helper.SetIndexes( +// registry, +// releaseChannel(), +// true, +// softwareUpdatesCurrentlyEnabled, +// intelUpdatesCurrentlyEnabled, +// ) +// if warning != nil { +// log.Warningf("updates: %s", warning) +// } - // Select versions depending on new indexes and modes. - registry.SelectVersions() - module.EventVersionsUpdated.Submit(struct{}{}) +// // Select versions depending on new indexes and modes. +// registry.SelectVersions() +// module.EventVersionsUpdated.Submit(struct{}{}) - if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { - module.states.Clear() - if err := TriggerUpdate(true, false); err != nil { - log.Warningf("updates: failed to trigger update: %s", err) - } - log.Infof("updates: automatic updates are now enabled") - } else { - log.Warningf("updates: automatic updates are now completely disabled") - } - } +// if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { +// module.states.Clear() +// if err := TriggerUpdate(true, false); err != nil { +// log.Warningf("updates: failed to trigger update: %s", err) +// } +// log.Infof("updates: automatic updates are now enabled") +// } else { +// log.Warningf("updates: automatic updates are now completely disabled") +// } +// } - return false, nil -} +// return false, nil +// } diff --git a/service/updates/export.go b/service/updates/export.go index c230f367..c736855c 100644 --- a/service/updates/export.go +++ b/service/updates/export.go @@ -1,238 +1,237 @@ package updates -import ( - "fmt" - "sort" - "strings" - "sync" +// import ( +// "fmt" +// "sort" +// "sync" - "github.com/safing/portmaster/base/database/record" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils/debug" - "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" -) +// "github.com/safing/portmaster/base/database/record" +// "github.com/safing/portmaster/base/info" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/updater" +// "github.com/safing/portmaster/base/utils/debug" +// "github.com/safing/portmaster/service/mgr" +// "github.com/safing/portmaster/service/updates/helper" +// ) -const ( - // versionsDBKey is the database key for update version information. - versionsDBKey = "core:status/versions" +// const ( +// // versionsDBKey is the database key for update version information. +// versionsDBKey = "core:status/versions" - // versionsDBKey is the database key for simple update version information. - simpleVersionsDBKey = "core:status/simple-versions" +// // versionsDBKey is the database key for simple update version information. +// simpleVersionsDBKey = "core:status/simple-versions" - // updateStatusDBKey is the database key for update status information. - updateStatusDBKey = "core:status/updates" -) +// // updateStatusDBKey is the database key for update status information. +// updateStatusDBKey = "core:status/updates" +// ) -// Versions holds update versions and status information. -type Versions struct { - record.Base - sync.Mutex +// // Versions holds update versions and status information. +// type Versions struct { +// record.Base +// sync.Mutex - Core *info.Info - Resources map[string]*updater.Resource - Channel string - Beta bool - Staging bool -} +// Core *info.Info +// Resources map[string]*updater.Resource +// Channel string +// Beta bool +// Staging bool +// } -// SimpleVersions holds simplified update versions and status information. -type SimpleVersions struct { - record.Base - sync.Mutex +// // SimpleVersions holds simplified update versions and status information. +// type SimpleVersions struct { +// record.Base +// sync.Mutex - Build *info.Info - Resources map[string]*SimplifiedResourceVersion - Channel string -} +// Build *info.Info +// Resources map[string]*SimplifiedResourceVersion +// Channel string +// } -// SimplifiedResourceVersion holds version information about one resource. -type SimplifiedResourceVersion struct { - Version string -} +// // SimplifiedResourceVersion holds version information about one resource. +// type SimplifiedResourceVersion struct { +// Version string +// } -// UpdateStateExport is a wrapper to export the updates state. -type UpdateStateExport struct { - record.Base - sync.Mutex +// // UpdateStateExport is a wrapper to export the updates state. +// type UpdateStateExport struct { +// record.Base +// sync.Mutex - *updater.UpdateState -} +// *updater.UpdateState +// } -// GetVersions returns the update versions and status information. -// Resources must be locked when accessed. -func GetVersions() *Versions { - return &Versions{ - Core: info.GetInfo(), - Resources: registry.Export(), - Channel: initialReleaseChannel, - Beta: initialReleaseChannel == helper.ReleaseChannelBeta, - Staging: initialReleaseChannel == helper.ReleaseChannelStaging, - } -} +// // GetVersions returns the update versions and status information. +// // Resources must be locked when accessed. +// func GetVersions() *Versions { +// return &Versions{ +// Core: info.GetInfo(), +// Resources: nil, +// Channel: initialReleaseChannel, +// Beta: initialReleaseChannel == helper.ReleaseChannelBeta, +// Staging: initialReleaseChannel == helper.ReleaseChannelStaging, +// } +// } -// GetSimpleVersions returns the simplified update versions and status information. -func GetSimpleVersions() *SimpleVersions { - // Fill base info. - v := &SimpleVersions{ - Build: info.GetInfo(), - Resources: make(map[string]*SimplifiedResourceVersion), - Channel: initialReleaseChannel, - } +// // GetSimpleVersions returns the simplified update versions and status information. +// func GetSimpleVersions() *SimpleVersions { +// // Fill base info. +// v := &SimpleVersions{ +// Build: info.GetInfo(), +// Resources: make(map[string]*SimplifiedResourceVersion), +// Channel: initialReleaseChannel, +// } - // Iterate through all versions and add version info. - for id, resource := range registry.Export() { - func() { - resource.Lock() - defer resource.Unlock() +// // Iterate through all versions and add version info. +// // for id, resource := range registry.Export() { +// // func() { +// // resource.Lock() +// // defer resource.Unlock() - // Get current in-used or selected version. - var rv *updater.ResourceVersion - switch { - case resource.ActiveVersion != nil: - rv = resource.ActiveVersion - case resource.SelectedVersion != nil: - rv = resource.SelectedVersion - } +// // // Get current in-used or selected version. +// // var rv *updater.ResourceVersion +// // switch { +// // case resource.ActiveVersion != nil: +// // rv = resource.ActiveVersion +// // case resource.SelectedVersion != nil: +// // rv = resource.SelectedVersion +// // } - // Get information from resource. - if rv != nil { - v.Resources[id] = &SimplifiedResourceVersion{ - Version: rv.VersionNumber, - } - } - }() - } +// // // Get information from resource. +// // if rv != nil { +// // v.Resources[id] = &SimplifiedResourceVersion{ +// // Version: rv.VersionNumber, +// // } +// // } +// // }() +// // } - return v -} +// return v +// } -// GetStateExport gets the update state from the registry and returns it in an -// exportable struct. -func GetStateExport() *UpdateStateExport { - export := registry.GetState() - return &UpdateStateExport{ - UpdateState: &export.Updates, - } -} +// // GetStateExport gets the update state from the registry and returns it in an +// // exportable struct. +// func GetStateExport() *UpdateStateExport { +// // export := registry.GetState() +// return &UpdateStateExport{ +// // UpdateState: &export.Updates, +// } +// } -// LoadStateExport loads the exported update state from the database. -func LoadStateExport() (*UpdateStateExport, error) { - r, err := db.Get(updateStatusDBKey) - if err != nil { - return nil, err - } +// // LoadStateExport loads the exported update state from the database. +// func LoadStateExport() (*UpdateStateExport, error) { +// r, err := db.Get(updateStatusDBKey) +// if err != nil { +// return nil, err +// } - // unwrap - if r.IsWrapped() { - // only allocate a new struct, if we need it - newRecord := &UpdateStateExport{} - err = record.Unwrap(r, newRecord) - if err != nil { - return nil, err - } - return newRecord, nil - } +// // unwrap +// if r.IsWrapped() { +// // only allocate a new struct, if we need it +// newRecord := &UpdateStateExport{} +// err = record.Unwrap(r, newRecord) +// if err != nil { +// return nil, err +// } +// return newRecord, nil +// } - // or adjust type - newRecord, ok := r.(*UpdateStateExport) - if !ok { - return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r) - } - return newRecord, nil -} +// // or adjust type +// newRecord, ok := r.(*UpdateStateExport) +// if !ok { +// return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r) +// } +// return newRecord, nil +// } -func initVersionExport() (err error) { - if err := GetVersions().save(); err != nil { - log.Warningf("updates: failed to export version information: %s", err) - } - if err := GetSimpleVersions().save(); err != nil { - log.Warningf("updates: failed to export version information: %s", err) - } +// func initVersionExport() (err error) { +// if err := GetVersions().save(); err != nil { +// log.Warningf("updates: failed to export version information: %s", err) +// } +// if err := GetSimpleVersions().save(); err != nil { +// log.Warningf("updates: failed to export version information: %s", err) +// } - module.EventVersionsUpdated.AddCallback("export version status", export) - return nil -} +// // module.EventVersionsUpdated.AddCallback("export version status", export) +// return nil +// } -func (v *Versions) save() error { - if !v.KeyIsSet() { - v.SetKey(versionsDBKey) - } - return db.Put(v) -} +// func (v *Versions) save() error { +// if !v.KeyIsSet() { +// v.SetKey(versionsDBKey) +// } +// return db.Put(v) +// } -func (v *SimpleVersions) save() error { - if !v.KeyIsSet() { - v.SetKey(simpleVersionsDBKey) - } - return db.Put(v) -} +// func (v *SimpleVersions) save() error { +// if !v.KeyIsSet() { +// v.SetKey(simpleVersionsDBKey) +// } +// return db.Put(v) +// } -func (s *UpdateStateExport) save() error { - if !s.KeyIsSet() { - s.SetKey(updateStatusDBKey) - } - return db.Put(s) -} +// func (s *UpdateStateExport) save() error { +// if !s.KeyIsSet() { +// s.SetKey(updateStatusDBKey) +// } +// return db.Put(s) +// } -// export is an event hook. -func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { - // Export versions. - if err := GetVersions().save(); err != nil { - return false, err - } - if err := GetSimpleVersions().save(); err != nil { - return false, err - } - // Export udpate state. - if err := GetStateExport().save(); err != nil { - return false, err - } +// // export is an event hook. +// func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { +// // Export versions. +// if err := GetVersions().save(); err != nil { +// return false, err +// } +// if err := GetSimpleVersions().save(); err != nil { +// return false, err +// } +// // Export udpate state. +// if err := GetStateExport().save(); err != nil { +// return false, err +// } - return false, nil -} +// return false, nil +// } -// AddToDebugInfo adds the update system status to the given debug.Info. -func AddToDebugInfo(di *debug.Info) { - // Get resources from registry. - resources := registry.Export() - platformPrefix := helper.PlatformIdentifier("") +// // AddToDebugInfo adds the update system status to the given debug.Info. +// func AddToDebugInfo(di *debug.Info) { +// // Get resources from registry. +// // resources := registry.Export() +// // platformPrefix := helper.PlatformIdentifier("") - // Collect data for debug info. - var active, selected []string - var activeCnt, totalCnt int - for id, r := range resources { - // Ignore resources for other platforms. - if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { - continue - } +// // Collect data for debug info. +// var active, selected []string +// var activeCnt, totalCnt int +// // for id, r := range resources { +// // // Ignore resources for other platforms. +// // if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { +// // continue +// // } - totalCnt++ - if r.ActiveVersion != nil { - activeCnt++ - active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) - } - if r.SelectedVersion != nil { - selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) - } - } - sort.Strings(active) - sort.Strings(selected) +// // totalCnt++ +// // if r.ActiveVersion != nil { +// // activeCnt++ +// // active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) +// // } +// // if r.SelectedVersion != nil { +// // selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) +// // } +// // } +// sort.Strings(active) +// sort.Strings(selected) - // Compile to one list. - lines := make([]string, 0, len(active)+len(selected)+3) - lines = append(lines, "Active:") - lines = append(lines, active...) - lines = append(lines, "") - lines = append(lines, "Selected:") - lines = append(lines, selected...) +// // Compile to one list. +// lines := make([]string, 0, len(active)+len(selected)+3) +// lines = append(lines, "Active:") +// lines = append(lines, active...) +// lines = append(lines, "") +// lines = append(lines, "Selected:") +// lines = append(lines, selected...) - // Add section. - di.AddSection( - fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), - debug.UseCodeSection|debug.AddContentLineBreaks, - lines..., - ) -} +// // Add section. +// di.AddSection( +// fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), +// debug.UseCodeSection|debug.AddContentLineBreaks, +// lines..., +// ) +// } diff --git a/service/updates/get.go b/service/updates/get.go index bac9ae14..75fc4c3e 100644 --- a/service/updates/get.go +++ b/service/updates/get.go @@ -1,72 +1,65 @@ package updates -import ( - "path" - - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates/helper" -) - // GetPlatformFile returns the latest platform specific file identified by the given identifier. -func GetPlatformFile(identifier string) (*updater.File, error) { - identifier = helper.PlatformIdentifier(identifier) +// func GetPlatformFile(identifier string) (*updater.File, error) { +// identifier = helper.PlatformIdentifier(identifier) - file, err := registry.GetFile(identifier) - if err != nil { - return nil, err - } +// file, err := registry.GetFile(identifier) +// if err != nil { +// return nil, err +// } - module.EventVersionsUpdated.Submit(struct{}{}) - return file, nil -} +// module.EventVersionsUpdated.Submit(struct{}{}) +// return file, nil +// } // GetFile returns the latest generic file identified by the given identifier. -func GetFile(identifier string) (*updater.File, error) { - identifier = path.Join("all", identifier) +// func GetFile(identifier string) (*updater.File, error) { +// identifier = path.Join("all", identifier) - file, err := registry.GetFile(identifier) - if err != nil { - return nil, err - } +// file, err := registry.GetFile(identifier) +// if err != nil { +// return nil, err +// } - module.EventVersionsUpdated.Submit(struct{}{}) - return file, nil -} +// module.EventVersionsUpdated.Submit(struct{}{}) +// return file, nil +// } // GetPlatformVersion returns the selected platform specific version of the // given identifier. // The returned resource version may not be modified. -func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) { - identifier = helper.PlatformIdentifier(identifier) +// func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) { +// identifier = helper.PlatformIdentifier(identifier) - rv, err := registry.GetVersion(identifier) - if err != nil { - return nil, err - } +// rv, err := registry.GetVersion(identifier) +// if err != nil { +// return nil, err +// } - return rv, nil -} +// return rv, nil +// } // GetVersion returns the selected generic version of the given identifier. // The returned resource version may not be modified. -func GetVersion(identifier string) (*updater.ResourceVersion, error) { - identifier = path.Join("all", identifier) +// func GetVersion(identifier string) (*updater.ResourceVersion, error) { +// identifier = path.Join("all", identifier) - rv, err := registry.GetVersion(identifier) - if err != nil { - return nil, err - } +// rv, err := registry.GetVersion(identifier) +// if err != nil { +// return nil, err +// } - return rv, nil -} +// return rv, nil +// } // GetVersionWithFullID returns the selected generic version of the given full identifier. // The returned resource version may not be modified. -func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { - rv, err := registry.GetVersion(identifier) - if err != nil { - return nil, err - } +// func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { +// rv, err := registry.GetVersion(identifier) +// if err != nil { +// return nil, err +// } - return rv, nil -} +// return rv, nil +// } diff --git a/service/updates/helper/electron.go b/service/updates/helper/electron.go index 4c8c4a07..a0c10149 100644 --- a/service/updates/helper/electron.go +++ b/service/updates/helper/electron.go @@ -1,57 +1,58 @@ package helper -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" +// import ( +// "errors" +// "fmt" +// "os" +// "path/filepath" +// "runtime" +// "strings" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" -) +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/updater" +// "github.com/safing/portmaster/service/updates/registry" +// ) -var pmElectronUpdate *updater.File +// var pmElectronUpdate *registry.File -const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` +// const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` -// EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed -// by our app-electron package has the SUID bit set on systems that do not -// allow unprivileged CLONE_NEWUSER (clone(3)). -// On non-linux systems or systems that have kernel.unprivileged_userns_clone -// set to 1 EnsureChromeSandboPermissions is a NO-OP. -func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { - if runtime.GOOS != "linux" { - return nil - } +// // EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed +// // by our app-electron package has the SUID bit set on systems that do not +// // allow unprivileged CLONE_NEWUSER (clone(3)). +// // On non-linux systems or systems that have kernel.unprivileged_userns_clone +// // set to 1 EnsureChromeSandboPermissions is a NO-OP. +// func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { +// if runtime.GOOS != "linux" { +// return nil +// } - if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { - return nil - } +// if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { +// return nil +// } - identifier := PlatformIdentifier("app/portmaster-app.zip") +// identifier := PlatformIdentifier("app/portmaster-app.zip") - var err error - pmElectronUpdate, err = reg.GetFile(identifier) - if err != nil { - if errors.Is(err, updater.ErrNotAvailableLocally) { - return nil - } - return fmt.Errorf("failed to get file: %w", err) - } +// var err error +// pmElectronUpdate, err = reg.GetFile(identifier) +// if err != nil { +// if errors.Is(err, updater.ErrNotAvailableLocally) { +// return nil +// } +// return fmt.Errorf("failed to get file: %w", err) +// } - unpackedPath := strings.TrimSuffix( - pmElectronUpdate.Path(), - filepath.Ext(pmElectronUpdate.Path()), - ) - sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") - if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { - log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) +// unpackedPath := strings.TrimSuffix( +// pmElectronUpdate.Path(), +// filepath.Ext(pmElectronUpdate.Path()), +// ) +// sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") +// if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { +// log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) - return fmt.Errorf("failed to chmod: %w", err) - } - log.Debugf("updates: fixed SUID permission for chrome-sandbox") +// return fmt.Errorf("failed to chmod: %w", err) +// } +// log.Debugf("updates: fixed SUID permission for chrome-sandbox") - return nil -} +// return nil +// } diff --git a/service/updates/helper/indexes.go b/service/updates/helper/indexes.go index 72457bc5..7b9e671e 100644 --- a/service/updates/helper/indexes.go +++ b/service/updates/helper/indexes.go @@ -1,136 +1,136 @@ package helper -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" +// import ( +// "errors" +// "fmt" +// "io/fs" +// "os" +// "path/filepath" - "github.com/safing/jess/filesig" - "github.com/safing/portmaster/base/updater" -) +// "github.com/safing/jess/filesig" +// "github.com/safing/portmaster/base/updater" +// ) -// Release Channel Configuration Keys. -const ( - ReleaseChannelKey = "core/releaseChannel" - ReleaseChannelJSONKey = "core.releaseChannel" -) +// // Release Channel Configuration Keys. +// const ( +// ReleaseChannelKey = "core/releaseChannel" +// ReleaseChannelJSONKey = "core.releaseChannel" +// ) -// Release Channels. -const ( - ReleaseChannelStable = "stable" - ReleaseChannelBeta = "beta" - ReleaseChannelStaging = "staging" - ReleaseChannelSupport = "support" -) +// // Release Channels. +// const ( +// ReleaseChannelStable = "stable" +// ReleaseChannelBeta = "beta" +// ReleaseChannelStaging = "staging" +// ReleaseChannelSupport = "support" +// ) -const jsonSuffix = ".json" +// const jsonSuffix = ".json" -// SetIndexes sets the update registry indexes and also configures the registry -// to use pre-releases based on the channel. -func SetIndexes( - registry *updater.ResourceRegistry, - releaseChannel string, - deleteUnusedIndexes bool, - autoDownload bool, - autoDownloadIntel bool, -) (warning error) { - usePreReleases := false +// // SetIndexes sets the update registry indexes and also configures the registry +// // to use pre-releases based on the channel. +// func SetIndexes( +// registry *updater.ResourceRegistry, +// releaseChannel string, +// deleteUnusedIndexes bool, +// autoDownload bool, +// autoDownloadIntel bool, +// ) (warning error) { +// usePreReleases := false - // Be reminded that the order is important, as indexes added later will - // override the current release from earlier indexes. +// // Be reminded that the order is important, as indexes added later will +// // override the current release from earlier indexes. - // Reset indexes before adding them (again). - registry.ResetIndexes() +// // Reset indexes before adding them (again). +// registry.ResetIndexes() - // Add the intel index first, in order to be able to override it with the - // other indexes when needed. - registry.AddIndex(updater.Index{ - Path: "all/intel/intel.json", - AutoDownload: autoDownloadIntel, - }) +// // Add the intel index first, in order to be able to override it with the +// // other indexes when needed. +// registry.AddIndex(updater.Index{ +// Path: "all/intel/intel.json", +// AutoDownload: autoDownloadIntel, +// }) - // Always add the stable index as a base. - registry.AddIndex(updater.Index{ - Path: ReleaseChannelStable + jsonSuffix, - AutoDownload: autoDownload, - }) +// // Always add the stable index as a base. +// registry.AddIndex(updater.Index{ +// Path: ReleaseChannelStable + jsonSuffix, +// AutoDownload: autoDownload, +// }) - // Add beta index if in beta or staging channel. - indexPath := ReleaseChannelBeta + jsonSuffix - if releaseChannel == ReleaseChannelBeta || - releaseChannel == ReleaseChannelStaging || - (releaseChannel == "" && indexExists(registry, indexPath)) { - registry.AddIndex(updater.Index{ - Path: indexPath, - PreRelease: true, - AutoDownload: autoDownload, - }) - usePreReleases = true - } else if deleteUnusedIndexes { - err := deleteIndex(registry, indexPath) - if err != nil { - warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) - } - } +// // Add beta index if in beta or staging channel. +// indexPath := ReleaseChannelBeta + jsonSuffix +// if releaseChannel == ReleaseChannelBeta || +// releaseChannel == ReleaseChannelStaging || +// (releaseChannel == "" && indexExists(registry, indexPath)) { +// registry.AddIndex(updater.Index{ +// Path: indexPath, +// PreRelease: true, +// AutoDownload: autoDownload, +// }) +// usePreReleases = true +// } else if deleteUnusedIndexes { +// err := deleteIndex(registry, indexPath) +// if err != nil { +// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) +// } +// } - // Add staging index if in staging channel. - indexPath = ReleaseChannelStaging + jsonSuffix - if releaseChannel == ReleaseChannelStaging || - (releaseChannel == "" && indexExists(registry, indexPath)) { - registry.AddIndex(updater.Index{ - Path: indexPath, - PreRelease: true, - AutoDownload: autoDownload, - }) - usePreReleases = true - } else if deleteUnusedIndexes { - err := deleteIndex(registry, indexPath) - if err != nil { - warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) - } - } +// // Add staging index if in staging channel. +// indexPath = ReleaseChannelStaging + jsonSuffix +// if releaseChannel == ReleaseChannelStaging || +// (releaseChannel == "" && indexExists(registry, indexPath)) { +// registry.AddIndex(updater.Index{ +// Path: indexPath, +// PreRelease: true, +// AutoDownload: autoDownload, +// }) +// usePreReleases = true +// } else if deleteUnusedIndexes { +// err := deleteIndex(registry, indexPath) +// if err != nil { +// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) +// } +// } - // Add support index if in support channel. - indexPath = ReleaseChannelSupport + jsonSuffix - if releaseChannel == ReleaseChannelSupport || - (releaseChannel == "" && indexExists(registry, indexPath)) { - registry.AddIndex(updater.Index{ - Path: indexPath, - AutoDownload: autoDownload, - }) - usePreReleases = true - } else if deleteUnusedIndexes { - err := deleteIndex(registry, indexPath) - if err != nil { - warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) - } - } +// // Add support index if in support channel. +// indexPath = ReleaseChannelSupport + jsonSuffix +// if releaseChannel == ReleaseChannelSupport || +// (releaseChannel == "" && indexExists(registry, indexPath)) { +// registry.AddIndex(updater.Index{ +// Path: indexPath, +// AutoDownload: autoDownload, +// }) +// usePreReleases = true +// } else if deleteUnusedIndexes { +// err := deleteIndex(registry, indexPath) +// if err != nil { +// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) +// } +// } - // Set pre-release usage. - registry.SetUsePreReleases(usePreReleases) +// // Set pre-release usage. +// registry.SetUsePreReleases(usePreReleases) - return warning -} +// return warning +// } -func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { - _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) - return err == nil -} +// func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { +// _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) +// return err == nil +// } -func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { - // Remove index itself. - err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } +// func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { +// // Remove index itself. +// err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) +// if err != nil && !errors.Is(err, fs.ErrNotExist) { +// return err +// } - // Remove any accompanying signature. - err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err - } +// // Remove any accompanying signature. +// err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) +// if err != nil && !errors.Is(err, fs.ErrNotExist) { +// return err +// } - return nil -} +// return nil +// } diff --git a/service/updates/helper/signing.go b/service/updates/helper/signing.go index 136b1970..9af98699 100644 --- a/service/updates/helper/signing.go +++ b/service/updates/helper/signing.go @@ -1,42 +1,42 @@ package helper -import ( - "github.com/safing/jess" - "github.com/safing/portmaster/base/updater" -) +// import ( +// "github.com/safing/jess" +// "github.com/safing/portmaster/base/updater" +// ) -var ( - // VerificationConfig holds the complete verification configuration for the registry. - VerificationConfig = map[string]*updater.VerificationOptions{ - "": { // Default. - TrustStore: BinarySigningTrustStore, - DownloadPolicy: updater.SignaturePolicyRequire, - DiskLoadPolicy: updater.SignaturePolicyWarn, - }, - "all/intel/": nil, // Disable until IntelHub supports signing. - } +// var ( +// // VerificationConfig holds the complete verification configuration for the registry. +// VerificationConfig = map[string]*updater.VerificationOptions{ +// "": { // Default. +// TrustStore: BinarySigningTrustStore, +// DownloadPolicy: updater.SignaturePolicyRequire, +// DiskLoadPolicy: updater.SignaturePolicyWarn, +// }, +// "all/intel/": nil, // Disable until IntelHub supports signing. +// } - // BinarySigningKeys holds the signing keys in text format. - BinarySigningKeys = []string{ - // Safing Code Signing Key #1 - "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", - // Safing Code Signing Key #2 - "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", - } +// // BinarySigningKeys holds the signing keys in text format. +// BinarySigningKeys = []string{ +// // Safing Code Signing Key #1 +// "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", +// // Safing Code Signing Key #2 +// "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", +// } - // BinarySigningTrustStore is an in-memory trust store with the signing keys. - BinarySigningTrustStore = jess.NewMemTrustStore() -) +// // BinarySigningTrustStore is an in-memory trust store with the signing keys. +// BinarySigningTrustStore = jess.NewMemTrustStore() +// ) -func init() { - for _, signingKey := range BinarySigningKeys { - rcpt, err := jess.RecipientFromTextFormat(signingKey) - if err != nil { - panic(err) - } - err = BinarySigningTrustStore.StoreSignet(rcpt) - if err != nil { - panic(err) - } - } -} +// func init() { +// for _, signingKey := range BinarySigningKeys { +// rcpt, err := jess.RecipientFromTextFormat(signingKey) +// if err != nil { +// panic(err) +// } +// err = BinarySigningTrustStore.StoreSignet(rcpt) +// if err != nil { +// panic(err) +// } +// } +// } diff --git a/service/updates/helper/updates.go b/service/updates/helper/updates.go index efae917d..135c1222 100644 --- a/service/updates/helper/updates.go +++ b/service/updates/helper/updates.go @@ -1,95 +1,95 @@ package helper -import ( - "fmt" - "runtime" +// import ( +// "fmt" +// "runtime" - "github.com/tevino/abool" -) +// "github.com/tevino/abool" +// ) -const onWindows = runtime.GOOS == "windows" +// const onWindows = runtime.GOOS == "windows" -var intelOnly = abool.New() +// var intelOnly = abool.New() -// IntelOnly specifies that only intel data is mandatory. -func IntelOnly() { - intelOnly.Set() -} +// // IntelOnly specifies that only intel data is mandatory. +// func IntelOnly() { +// intelOnly.Set() +// } -// PlatformIdentifier converts identifier for the current platform. -func PlatformIdentifier(identifier string) string { - // From https://golang.org/pkg/runtime/#GOARCH - // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. - // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. - return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) -} +// // PlatformIdentifier converts identifier for the current platform. +// func PlatformIdentifier(identifier string) string { +// // From https://golang.org/pkg/runtime/#GOARCH +// // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. +// // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. +// return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) +// } -// MandatoryUpdates returns mandatory updates that should be loaded on install -// or reset. -func MandatoryUpdates() (identifiers []string) { - // Intel - identifiers = append( - identifiers, +// // MandatoryUpdates returns mandatory updates that should be loaded on install +// // or reset. +// func MandatoryUpdates() (identifiers []string) { +// // Intel +// identifiers = append( +// identifiers, - // Filter lists data - "all/intel/lists/index.dsd", - "all/intel/lists/base.dsdl", - "all/intel/lists/intermediate.dsdl", - "all/intel/lists/urgent.dsdl", +// // Filter lists data +// "all/intel/lists/index.dsd", +// "all/intel/lists/base.dsdl", +// "all/intel/lists/intermediate.dsdl", +// "all/intel/lists/urgent.dsdl", - // Geo IP data - "all/intel/geoip/geoipv4.mmdb.gz", - "all/intel/geoip/geoipv6.mmdb.gz", - ) +// // Geo IP data +// "all/intel/geoip/geoipv4.mmdb.gz", +// "all/intel/geoip/geoipv6.mmdb.gz", +// ) - // Stop here if we only want intel data. - if intelOnly.IsSet() { - return identifiers - } +// // Stop here if we only want intel data. +// if intelOnly.IsSet() { +// return identifiers +// } - // Binaries - if onWindows { - identifiers = append( - identifiers, - PlatformIdentifier("core/portmaster-core.exe"), - PlatformIdentifier("kext/portmaster-kext.sys"), - PlatformIdentifier("kext/portmaster-kext.pdb"), - PlatformIdentifier("start/portmaster-start.exe"), - PlatformIdentifier("notifier/portmaster-notifier.exe"), - PlatformIdentifier("notifier/portmaster-wintoast.dll"), - PlatformIdentifier("app2/portmaster-app.zip"), - ) - } else { - identifiers = append( - identifiers, - PlatformIdentifier("core/portmaster-core"), - PlatformIdentifier("start/portmaster-start"), - PlatformIdentifier("notifier/portmaster-notifier"), - PlatformIdentifier("app2/portmaster-app"), - ) - } +// // Binaries +// if onWindows { +// identifiers = append( +// identifiers, +// PlatformIdentifier("core/portmaster-core.exe"), +// PlatformIdentifier("kext/portmaster-kext.sys"), +// PlatformIdentifier("kext/portmaster-kext.pdb"), +// PlatformIdentifier("start/portmaster-start.exe"), +// PlatformIdentifier("notifier/portmaster-notifier.exe"), +// PlatformIdentifier("notifier/portmaster-wintoast.dll"), +// PlatformIdentifier("app2/portmaster-app.zip"), +// ) +// } else { +// identifiers = append( +// identifiers, +// PlatformIdentifier("core/portmaster-core"), +// PlatformIdentifier("start/portmaster-start"), +// PlatformIdentifier("notifier/portmaster-notifier"), +// PlatformIdentifier("app2/portmaster-app"), +// ) +// } - // Components, Assets and Data - identifiers = append( - identifiers, +// // Components, Assets and Data +// identifiers = append( +// identifiers, - // User interface components - PlatformIdentifier("app/portmaster-app.zip"), - "all/ui/modules/portmaster.zip", - "all/ui/modules/assets.zip", - ) +// // User interface components +// PlatformIdentifier("app/portmaster-app.zip"), +// "all/ui/modules/portmaster.zip", +// "all/ui/modules/assets.zip", +// ) - return identifiers -} +// return identifiers +// } -// AutoUnpackUpdates returns assets that need unpacking. -func AutoUnpackUpdates() []string { - if intelOnly.IsSet() { - return []string{} - } +// // AutoUnpackUpdates returns assets that need unpacking. +// func AutoUnpackUpdates() []string { +// if intelOnly.IsSet() { +// return []string{} +// } - return []string{ - PlatformIdentifier("app/portmaster-app.zip"), - PlatformIdentifier("app2/portmaster-app.zip"), - } -} +// return []string{ +// PlatformIdentifier("app/portmaster-app.zip"), +// PlatformIdentifier("app2/portmaster-app.zip"), +// } +// } diff --git a/service/updates/index.go b/service/updates/index.go deleted file mode 100644 index adf4564f..00000000 --- a/service/updates/index.go +++ /dev/null @@ -1,110 +0,0 @@ -package updates - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - - "github.com/safing/portmaster/base/log" -) - -type UpdateIndex struct { - Directory string - DownloadDirectory string - Ignore []string - IndexURLs []string - IndexFile string - AutoApply bool -} - -func (ui *UpdateIndex) downloadIndexFile() (err error) { - _ = os.MkdirAll(ui.Directory, defaultDirMode) - _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) - for _, url := range ui.IndexURLs { - err = ui.downloadIndexFileFromURL(url) - if err != nil { - log.Warningf("updates: %s", err) - continue - } - // Downloading was successful. - err = nil - break - } - return -} - -func (ui *UpdateIndex) checkForUpdates() (bool, error) { - err := ui.downloadIndexFile() - if err != nil { - return false, err - } - - currentBundle, err := ui.GetInstallBundle() - if err != nil { - return true, err // Current installed bundle not found, act as there is update. - } - updateBundle, err := ui.GetUpdateBundle() - if err != nil { - return false, err - } - - return currentBundle.Version != updateBundle.Version, nil -} - -func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { - client := http.Client{} - resp, err := client.Get(url) - if err != nil { - return fmt.Errorf("failed a get request to %s: %w", url, err) - } - defer func() { _ = resp.Body.Close() }() - filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) - file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) - if err != nil { - return err - } - defer func() { _ = file.Close() }() - - _, err = io.Copy(file, resp.Body) - if err != nil { - return err - } - - return nil -} - -func (ui *UpdateIndex) GetInstallBundle() (*Bundle, error) { - indexFile := fmt.Sprintf("%s/%s", ui.Directory, ui.IndexFile) - return ui.GetBundle(indexFile) -} - -func (ui *UpdateIndex) GetUpdateBundle() (*Bundle, error) { - indexFile := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) - return ui.GetBundle(indexFile) -} - -func (ui *UpdateIndex) GetBundle(indexFile string) (*Bundle, error) { - // Check if the file exists. - file, err := os.Open(indexFile) - if err != nil { - return nil, fmt.Errorf("failed to open index file: %w", err) - } - defer func() { _ = file.Close() }() - - // Read - content, err := io.ReadAll(file) - if err != nil { - return nil, err - } - - // Parse - var bundle Bundle - err = json.Unmarshal(content, &bundle) - if err != nil { - return nil, err - } - - return &bundle, nil -} diff --git a/service/updates/main.go b/service/updates/main.go index 3e11064c..f9e68b8b 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -6,9 +6,6 @@ import ( "time" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/mgr" ) const ( @@ -17,10 +14,6 @@ const ( enableSoftwareUpdatesKey = "core/automaticUpdates" enableIntelUpdatesKey = "core/automaticIntelUpdates" - // ModuleName is the name of the update module - // and can be used when declaring module dependencies. - ModuleName = "updates" - // VersionUpdateEvent is emitted every time a new // version of a monitored resource is selected. // During module initialization VersionUpdateEvent @@ -37,8 +30,6 @@ const ( ) var ( - registry *updater.ResourceRegistry - userAgentFromFlag string updateServerFromFlag string @@ -57,205 +48,13 @@ const ( updateTaskRepeatDuration = 1 * time.Hour ) -func start() error { - // module.restartWorkerMgr.Repeat(10 * time.Minute) - // module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig) - - // // create registry - // registry = &updater.ResourceRegistry{ - // Name: ModuleName, - // UpdateURLs: DefaultUpdateURLs, - // UserAgent: UserAgent, - // MandatoryUpdates: helper.MandatoryUpdates(), - // AutoUnpack: helper.AutoUnpackUpdates(), - // Verification: helper.VerificationConfig, - // DevMode: devMode(), - // Online: true, - // } - // // Override values from flags. - // if userAgentFromFlag != "" { - // registry.UserAgent = userAgentFromFlag - // } - // if updateServerFromFlag != "" { - // registry.UpdateURLs = []string{updateServerFromFlag} - // } - - // // pre-init state - // updateStateExport, err := LoadStateExport() - // if err != nil { - // log.Debugf("updates: failed to load exported update state: %s", err) - // } else if updateStateExport.UpdateState != nil { - // err := registry.PreInitUpdateState(*updateStateExport.UpdateState) - // if err != nil { - // return err - // } - // } - - // initialize - // err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) - // if err != nil { - // return err - // } - - // // register state provider - // err = registerRegistryStateProvider() - // if err != nil { - // return err - // } - // registry.StateNotifyFunc = pushRegistryState - - // // Set indexes based on the release channel. - // warning := helper.SetIndexes( - // registry, - // initialReleaseChannel, - // true, - // enableSoftwareUpdates() && !DisableSoftwareAutoUpdate, - // enableIntelUpdates(), - // ) - // if warning != nil { - // log.Warningf("updates: %s", warning) - // } - - // err = registry.LoadIndexes(module.m.Ctx()) - // if err != nil { - // log.Warningf("updates: failed to load indexes: %s", err) - // } - - // err = registry.ScanStorage("") - // if err != nil { - // log.Warningf("updates: error during storage scan: %s", err) - // } - - // registry.SelectVersions() - // module.EventVersionsUpdated.Submit(struct{}{}) - - // // Initialize the version export - this requires the registry to be set up. - // err = initVersionExport() - // if err != nil { - // return err - // } - - // // start updater task - // if !disableTaskSchedule { - // _ = module.updateWorkerMgr.Repeat(30 * time.Minute) - // } - - // if updateASAP { - // module.updateWorkerMgr.Go() - // } - - // // react to upgrades - // if err := initUpgrader(); err != nil { - // return err - // } - - // warnOnIncorrectParentPath() - - return nil -} - -// TriggerUpdate queues the update task to execute ASAP. -func TriggerUpdate(forceIndexCheck, downloadAll bool) error { - // switch { - // case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): - // return errors.New("automatic updating is disabled") - - // default: - // if forceIndexCheck { - // forceCheck.Set() - // } - // if downloadAll { - // forceDownload.Set() - // } - - // // If index check if forced, start quicker. - // module.updateWorkerMgr.Go() - // } - - log.Debugf("updates: triggering update to run as soon as possible") - return nil -} - -// DisableUpdateSchedule disables the update schedule. -// If called, updates are only checked when TriggerUpdate() -// is called. -func DisableUpdateSchedule() error { - // TODO: Updater state should be always on - // switch module.Status() { - // case modules.StatusStarting, modules.StatusOnline, modules.StatusStopping: - // return errors.New("module already online") - // } - - return nil -} - -func checkForUpdates(ctx *mgr.WorkerCtx) (err error) { - // Set correct error if context was canceled. - // defer func() { - // select { - // case <-ctx.Done(): - // err = context.Canceled - // default: - // } - // }() - - // // Get flags. - // forceIndexCheck := forceCheck.SetToIf(true, false) - // downloadAll := forceDownload.SetToIf(true, false) - - // // Check again if downloading updates is enabled, or forced. - // if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() { - // log.Warningf("updates: automatic updates are disabled") - // return nil - // } - - // defer func() { - // // Resolve any error and send success notification. - // if err == nil { - // log.Infof("updates: successfully checked for updates") - // notifyUpdateSuccess(forceIndexCheck) - // return - // } - - // // Log and notify error. - // log.Errorf("updates: check failed: %s", err) - // notifyUpdateCheckFailed(forceIndexCheck, err) - // }() - - // if err = registry.UpdateIndexes(ctx.Ctx()); err != nil { - // err = fmt.Errorf("failed to update indexes: %w", err) - // return //nolint:nakedret // TODO: Would "return err" work with the defer? - // } - - // err = registry.DownloadUpdates(ctx.Ctx(), downloadAll) - // if err != nil { - // err = fmt.Errorf("failed to download updates: %w", err) - // return //nolint:nakedret // TODO: Would "return err" work with the defer? - // } - - // registry.SelectVersions() - - // // Unpack selected resources. - // err = registry.UnpackResources() - // if err != nil { - // err = fmt.Errorf("failed to unpack updates: %w", err) - // return //nolint:nakedret // TODO: Would "return err" work with the defer? - // } - - // // Purge old resources - // registry.Purge(2) - - // module.EventResourcesUpdated.Submit(struct{}{}) - return nil -} - func stop() error { - if registry != nil { - err := registry.Cleanup() - if err != nil { - log.Warningf("updates: failed to clean up registry: %s", err) - } - } + // if registry != nil { + // err := registry.Cleanup() + // if err != nil { + // log.Warningf("updates: failed to clean up registry: %s", err) + // } + // } return nil } diff --git a/service/updates/module.go b/service/updates/module.go index 002e315e..989ad020 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -2,10 +2,8 @@ package updates import ( "errors" + "flag" "fmt" - "os" - "path/filepath" - "strings" "sync/atomic" "github.com/safing/portmaster/base/api" @@ -13,34 +11,33 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates/registry" ) -const ( - defaultFileMode = os.FileMode(0o0644) - defaultDirMode = os.FileMode(0o0755) -) +var applyUpdates bool + +func init() { + flag.BoolVar(&applyUpdates, "update", false, "apply downloaded updates") +} // Updates provides access to released artifacts. type Updates struct { m *mgr.Manager states *mgr.StateMgr - updateWorkerMgr *mgr.WorkerMgr - restartWorkerMgr *mgr.WorkerMgr + updateBinaryWorkerMgr *mgr.WorkerMgr + updateIntelWorkerMgr *mgr.WorkerMgr + restartWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] - binUpdates UpdateIndex - intelUpdates UpdateIndex + registry registry.Registry instance instance } -var ( - module *Updates - shimLoaded atomic.Bool -) +var shimLoaded atomic.Bool // New returns a new UI module. func New(instance instance) (*Updates, error) { @@ -49,20 +46,22 @@ func New(instance instance) (*Updates, error) { } m := mgr.New("Updates") - module = &Updates{ + module := &Updates{ m: m, states: m.NewStateMgr(), EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), - instance: instance, + + instance: instance, } // Events - module.updateWorkerMgr = m.NewWorkerMgr("updater", module.checkForUpdates, nil) + module.updateBinaryWorkerMgr = m.NewWorkerMgr("binary updater", module.checkForBinaryUpdates, nil) + module.updateIntelWorkerMgr = m.NewWorkerMgr("intel updater", module.checkForIntelUpdates, nil) module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) - module.binUpdates = UpdateIndex{ + binIndex := registry.UpdateIndex{ Directory: "/usr/lib/portmaster", DownloadDirectory: "/var/portmaster/new_bin", Ignore: []string{"databases", "intel", "config.json"}, @@ -71,62 +70,48 @@ func New(instance instance) (*Updates, error) { AutoApply: false, } - module.intelUpdates = UpdateIndex{ + intelIndex := registry.UpdateIndex{ Directory: "/var/portmaster/intel", DownloadDirectory: "/var/portmaster/new_intel", IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, } + module.registry = registry.New(binIndex, intelIndex) return module, nil } -func deleteUnfinishedDownloads(rootDir string) error { - return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { +func (u *Updates) checkForBinaryUpdates(_ *mgr.WorkerCtx) error { + hasUpdates, err := u.registry.CheckForBinaryUpdates() + if err != nil { + log.Errorf("updates: failed to check for binary updates: %s", err) + } + if hasUpdates { + log.Infof("updates: there is updates available in the binary bundle") + err = u.registry.DownloadBinaryUpdates() if err != nil { - return err + log.Errorf("updates: failed to download bundle: %s", err) } - - // Check if the current file has the specified extension - if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { - log.Warningf("updates deleting unfinished: %s\n", path) - err := os.Remove(path) - if err != nil { - return fmt.Errorf("failed to delete file %s: %w", path, err) - } - } - - return nil - }) + } else { + log.Infof("updates: no new binary updates") + } + return nil } -func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { - _ = deleteUnfinishedDownloads(u.binUpdates.DownloadDirectory) - hasUpdate, err := u.binUpdates.checkForUpdates() +func (u *Updates) checkForIntelUpdates(_ *mgr.WorkerCtx) error { + hasUpdates, err := u.registry.CheckForIntelUpdates() if err != nil { - log.Warningf("failed to get binary index file: %s", err) + log.Errorf("updates: failed to check for intel updates: %s", err) } - if hasUpdate { - binBundle, err := u.binUpdates.GetUpdateBundle() - if err == nil { - log.Debugf("Bin Bundle: %+v", binBundle) - _ = os.MkdirAll(u.binUpdates.DownloadDirectory, defaultDirMode) - binBundle.downloadAndVerify(u.binUpdates.DownloadDirectory) - } - } - _ = deleteUnfinishedDownloads(u.intelUpdates.DownloadDirectory) - hasUpdate, err = u.intelUpdates.checkForUpdates() - if err != nil { - log.Warningf("failed to get intel index file: %s", err) - } - if hasUpdate { - intelBundle, err := u.intelUpdates.GetUpdateBundle() - if err == nil { - log.Debugf("Intel Bundle: %+v", intelBundle) - _ = os.MkdirAll(u.intelUpdates.DownloadDirectory, defaultDirMode) - intelBundle.downloadAndVerify(u.intelUpdates.DownloadDirectory) + if hasUpdates { + log.Infof("updates: there is updates available in the intel bundle") + err = u.registry.DownloadIntelUpdates() + if err != nil { + log.Errorf("updates: failed to download bundle: %s", err) } + } else { + log.Infof("updates: no new intel data updates") } return nil } @@ -143,38 +128,36 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { - initConfig() - u.m.Go("check for updates", func(w *mgr.WorkerCtx) error { - binBundle, err := u.binUpdates.GetInstallBundle() - if err != nil { - log.Warningf("failed to get binary bundle: %s", err) - } else { - err = binBundle.Verify(u.binUpdates.Directory) - if err != nil { - log.Warningf("binary bundle is not valid: %s", err) - } else { - log.Infof("binary bundle is valid") - } - } + // initConfig() - intelBundle, err := u.intelUpdates.GetInstallBundle() + if applyUpdates { + err := u.registry.ApplyBinaryUpdates() if err != nil { - log.Warningf("failed to get intel bundle: %s", err) - } else { - err = intelBundle.Verify(u.intelUpdates.Directory) - if err != nil { - log.Warningf("intel bundle is not valid: %s", err) - } else { - log.Infof("intel bundle is valid") - } + log.Errorf("updates: failed to apply binary updates: %s", err) } - + err = u.registry.ApplyIntelUpdates() + if err != nil { + log.Errorf("updates: failed to apply intel updates: %s", err) + } + u.instance.Restart() return nil - }) - u.updateWorkerMgr.Go() + } + + err := u.registry.Initialize() + if err != nil { + // TODO(vladimir): Find a better way to handle this error. The service will stop if parsing of the bundle files fails. + return fmt.Errorf("failed to initialize registry: %w", err) + } + + u.updateBinaryWorkerMgr.Go() + u.updateIntelWorkerMgr.Go() return nil } +func (u *Updates) GetFile(id string) (*registry.File, error) { + return u.registry.GetFile(id) +} + // Stop stops the module. func (u *Updates) Stop() error { return stop() diff --git a/service/updates/notify.go b/service/updates/notify.go index 076eea34..1a539f7a 100644 --- a/service/updates/notify.go +++ b/service/updates/notify.go @@ -1,12 +1,8 @@ package updates import ( - "fmt" - "strings" "sync/atomic" "time" - - "github.com/safing/portmaster/base/notifications" ) const ( @@ -25,109 +21,109 @@ func (u *Updates) notificationsEnabled() bool { return u.instance.Notifications() != nil } -func notifyUpdateSuccess(force bool) { - if !module.notificationsEnabled() { - return - } +// func notifyUpdateSuccess(force bool) { +// if !module.notificationsEnabled() { +// return +// } - updateFailedCnt.Store(0) - module.states.Clear() - updateState := registry.GetState().Updates +// updateFailedCnt.Store(0) +// module.states.Clear() +// updateState := registry.GetState().Updates - flavor := updateSuccess - switch { - case len(updateState.PendingDownload) > 0: - // Show notification if there are pending downloads. - flavor = updateSuccessPending - case updateState.LastDownloadAt != nil && - time.Since(*updateState.LastDownloadAt) < 5*time.Second: - // Show notification if we downloaded something within the last minute. - flavor = updateSuccessDownloaded - case force: - // Always show notification if update was manually triggered. - default: - // Otherwise, the update was uneventful. Do not show notification. - return - } +// flavor := updateSuccess +// switch { +// case len(updateState.PendingDownload) > 0: +// // Show notification if there are pending downloads. +// flavor = updateSuccessPending +// case updateState.LastDownloadAt != nil && +// time.Since(*updateState.LastDownloadAt) < 5*time.Second: +// // Show notification if we downloaded something within the last minute. +// flavor = updateSuccessDownloaded +// case force: +// // Always show notification if update was manually triggered. +// default: +// // Otherwise, the update was uneventful. Do not show notification. +// return +// } - switch flavor { - case updateSuccess: - notifications.Notify(¬ifications.Notification{ - EventID: updateSuccess, - Type: notifications.Info, - Title: "Portmaster Is Up-To-Date", - Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), - Expires: time.Now().Add(1 * time.Minute).Unix(), - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", - }, - }, - }) +// switch flavor { +// case updateSuccess: +// notifications.Notify(¬ifications.Notification{ +// EventID: updateSuccess, +// Type: notifications.Info, +// Title: "Portmaster Is Up-To-Date", +// Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), +// Expires: time.Now().Add(1 * time.Minute).Unix(), +// AvailableActions: []*notifications.Action{ +// { +// ID: "ack", +// Text: "OK", +// }, +// }, +// }) - case updateSuccessPending: - msg := fmt.Sprintf( - `%d updates are available for download: +// case updateSuccessPending: +// msg := fmt.Sprintf( +// `%d updates are available for download: -- %s +// - %s -Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, - len(updateState.PendingDownload), - strings.Join(updateState.PendingDownload, "\n- "), - ) +// Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, +// len(updateState.PendingDownload), +// strings.Join(updateState.PendingDownload, "\n- "), +// ) - notifications.Notify(¬ifications.Notification{ - EventID: updateSuccess, - Type: notifications.Info, - Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), - Message: msg, - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", - }, - { - ID: "download", - Text: "Download Now", - Type: notifications.ActionTypeWebhook, - Payload: ¬ifications.ActionTypeWebhookPayload{ - URL: apiPathCheckForUpdates + "?download", - ResultAction: "display", - }, - }, - }, - }) +// notifications.Notify(¬ifications.Notification{ +// EventID: updateSuccess, +// Type: notifications.Info, +// Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), +// Message: msg, +// AvailableActions: []*notifications.Action{ +// { +// ID: "ack", +// Text: "OK", +// }, +// { +// ID: "download", +// Text: "Download Now", +// Type: notifications.ActionTypeWebhook, +// Payload: ¬ifications.ActionTypeWebhookPayload{ +// URL: apiPathCheckForUpdates + "?download", +// ResultAction: "display", +// }, +// }, +// }, +// }) - case updateSuccessDownloaded: - msg := fmt.Sprintf( - `%d updates were downloaded and applied: +// case updateSuccessDownloaded: +// msg := fmt.Sprintf( +// `%d updates were downloaded and applied: -- %s +// - %s -%s -`, - len(updateState.LastDownload), - strings.Join(updateState.LastDownload, "\n- "), - getUpdatingInfoMsg(), - ) +// %s +// `, +// len(updateState.LastDownload), +// strings.Join(updateState.LastDownload, "\n- "), +// getUpdatingInfoMsg(), +// ) - notifications.Notify(¬ifications.Notification{ - EventID: updateSuccess, - Type: notifications.Info, - Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), - Message: msg, - Expires: time.Now().Add(1 * time.Minute).Unix(), - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", - }, - }, - }) +// notifications.Notify(¬ifications.Notification{ +// EventID: updateSuccess, +// Type: notifications.Info, +// Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), +// Message: msg, +// Expires: time.Now().Add(1 * time.Minute).Unix(), +// AvailableActions: []*notifications.Action{ +// { +// ID: "ack", +// Text: "OK", +// }, +// }, +// }) - } -} +// } +// } func getUpdatingInfoMsg() string { switch { @@ -140,41 +136,41 @@ func getUpdatingInfoMsg() string { } } -func notifyUpdateCheckFailed(force bool, err error) { - if !module.notificationsEnabled() { - return - } +// func notifyUpdateCheckFailed(force bool, err error) { +// if !module.notificationsEnabled() { +// return +// } - failedCnt := updateFailedCnt.Add(1) - lastSuccess := registry.GetState().Updates.LastSuccessAt +// failedCnt := updateFailedCnt.Add(1) +// lastSuccess := registry.GetState().Updates.LastSuccessAt - switch { - case force: - // Always show notification if update was manually triggered. - case failedCnt < failedUpdateNotifyCountThreshold: - // Not failed often enough for notification. - return - case lastSuccess == nil: - // No recorded successful update. - case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess): - // Failed too recently for notification. - return - } +// switch { +// case force: +// // Always show notification if update was manually triggered. +// case failedCnt < failedUpdateNotifyCountThreshold: +// // Not failed often enough for notification. +// return +// case lastSuccess == nil: +// // No recorded successful update. +// case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess): +// // Failed too recently for notification. +// return +// } - notifications.NotifyWarn( - updateFailed, - "Update Check Failed", - fmt.Sprintf( - "Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s", - err, - ), - notifications.Action{ - Text: "Try Again Now", - Type: notifications.ActionTypeWebhook, - Payload: ¬ifications.ActionTypeWebhookPayload{ - URL: apiPathCheckForUpdates, - ResultAction: "display", - }, - }, - ).SyncWithState(module.states) -} +// notifications.NotifyWarn( +// updateFailed, +// "Update Check Failed", +// fmt.Sprintf( +// "Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s", +// err, +// ), +// notifications.Action{ +// Text: "Try Again Now", +// Type: notifications.ActionTypeWebhook, +// Payload: ¬ifications.ActionTypeWebhookPayload{ +// URL: apiPathCheckForUpdates, +// ResultAction: "display", +// }, +// }, +// ).SyncWithState(module.states) +// } diff --git a/service/updates/os_integration_linux.go b/service/updates/os_integration_linux.go index cef0b9ef..cd1b6137 100644 --- a/service/updates/os_integration_linux.go +++ b/service/updates/os_integration_linux.go @@ -1,204 +1,201 @@ package updates -import ( - "bytes" - "crypto/sha256" - _ "embed" - "encoding/hex" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" +// import ( +// "crypto/sha256" +// _ "embed" +// "encoding/hex" +// "errors" +// "fmt" +// "io/fs" +// "os" +// "path/filepath" - "github.com/tevino/abool" - "golang.org/x/exp/slices" +// "github.com/tevino/abool" +// "golang.org/x/exp/slices" - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils/renameio" -) +// "github.com/safing/portmaster/base/dataroot" +// "github.com/safing/portmaster/base/log" +// ) -var ( - portmasterCoreServiceFilePath = "portmaster.service" - portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" - backupExtension = ".backup" +// var ( +// portmasterCoreServiceFilePath = "portmaster.service" +// portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" +// backupExtension = ".backup" - //go:embed assets/portmaster.service - currentPortmasterCoreServiceFile []byte +// //go:embed assets/portmaster.service +// currentPortmasterCoreServiceFile []byte - checkedSystemIntegration = abool.New() +// checkedSystemIntegration = abool.New() - // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. - ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") -) +// // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. +// ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") +// ) -func upgradeSystemIntegration() { - // Check if we already checked the system integration. - if !checkedSystemIntegration.SetToIf(false, true) { - return - } +// func upgradeSystemIntegration() { +// // Check if we already checked the system integration. +// if !checkedSystemIntegration.SetToIf(false, true) { +// return +// } - // Upgrade portmaster core systemd service. - err := upgradeSystemIntegrationFile( - "portmaster core systemd service", - filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), - 0o0600, - currentPortmasterCoreServiceFile, - []string{ - "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 - "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 - "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 - }, - ) - if err != nil { - log.Warningf("updates: %s", err) - return - } +// // Upgrade portmaster core systemd service. +// err := upgradeSystemIntegrationFile( +// "portmaster core systemd service", +// filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), +// 0o0600, +// currentPortmasterCoreServiceFile, +// []string{ +// "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 +// "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 +// "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 +// }, +// ) +// if err != nil { +// log.Warningf("updates: %s", err) +// return +// } - // Upgrade portmaster notifier systemd user service. - // Permissions only! - err = upgradeSystemIntegrationFile( - "portmaster notifier systemd user service", - filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), - 0o0644, - nil, // Do not update contents. - nil, // Do not update contents. - ) - if err != nil { - log.Warningf("updates: %s", err) - return - } -} +// // Upgrade portmaster notifier systemd user service. +// // Permissions only! +// err = upgradeSystemIntegrationFile( +// "portmaster notifier systemd user service", +// filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), +// 0o0644, +// nil, // Do not update contents. +// nil, // Do not update contents. +// ) +// if err != nil { +// log.Warningf("updates: %s", err) +// return +// } +// } -// upgradeSystemIntegrationFile upgrades the file contents and permissions. -// System integration files are not necessarily present and may also be -// edited by third parties, such as the OS itself or other installers. -// The supplied hashes must be sha256 hex-encoded. -func upgradeSystemIntegrationFile( - name string, - filePath string, - fileMode fs.FileMode, - fileData []byte, - permittedUpgradeHashes []string, -) error { - // Upgrade file contents. - if len(fileData) > 0 { - if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { - return err - } - } +// // upgradeSystemIntegrationFile upgrades the file contents and permissions. +// // System integration files are not necessarily present and may also be +// // edited by third parties, such as the OS itself or other installers. +// // The supplied hashes must be sha256 hex-encoded. +// func upgradeSystemIntegrationFile( +// name string, +// filePath string, +// fileMode fs.FileMode, +// fileData []byte, +// permittedUpgradeHashes []string, +// ) error { +// // Upgrade file contents. +// if len(fileData) > 0 { +// if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { +// return err +// } +// } - // Upgrade file permissions. - if fileMode != 0 { - if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { - return err - } - } +// // Upgrade file permissions. +// if fileMode != 0 { +// if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { +// return err +// } +// } - return nil -} +// return nil +// } -// upgradeSystemIntegrationFileContents upgrades the file contents. -// System integration files are not necessarily present and may also be -// edited by third parties, such as the OS itself or other installers. -// The supplied hashes must be sha256 hex-encoded. -func upgradeSystemIntegrationFileContents( - name string, - filePath string, - fileData []byte, - permittedUpgradeHashes []string, -) error { - // Read existing file. - existingFileData, err := os.ReadFile(filePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) - } +// // upgradeSystemIntegrationFileContents upgrades the file contents. +// // System integration files are not necessarily present and may also be +// // edited by third parties, such as the OS itself or other installers. +// // The supplied hashes must be sha256 hex-encoded. +// func upgradeSystemIntegrationFileContents( +// name string, +// filePath string, +// fileData []byte, +// permittedUpgradeHashes []string, +// ) error { +// // Read existing file. +// existingFileData, err := os.ReadFile(filePath) +// if err != nil { +// if errors.Is(err, os.ErrNotExist) { +// return nil +// } +// return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) +// } - // Check if file is already the current version. - existingSum := sha256.Sum256(existingFileData) - existingHexSum := hex.EncodeToString(existingSum[:]) - currentSum := sha256.Sum256(fileData) - currentHexSum := hex.EncodeToString(currentSum[:]) - if existingHexSum == currentHexSum { - log.Debugf("updates: %s at %s is up to date", name, filePath) - return nil - } +// // Check if file is already the current version. +// existingSum := sha256.Sum256(existingFileData) +// existingHexSum := hex.EncodeToString(existingSum[:]) +// currentSum := sha256.Sum256(fileData) +// currentHexSum := hex.EncodeToString(currentSum[:]) +// if existingHexSum == currentHexSum { +// log.Debugf("updates: %s at %s is up to date", name, filePath) +// return nil +// } - // Check if we are allowed to upgrade from the existing file. - if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { - return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) - } +// // Check if we are allowed to upgrade from the existing file. +// if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { +// return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) +// } - // Start with upgrade! +// // Start with upgrade! - // Make backup of existing file. - err = CopyFile(filePath, filePath+backupExtension) - if err != nil { - return fmt.Errorf( - "failed to create backup of %s from %s to %s: %w", - name, - filePath, - filePath+backupExtension, - err, - ) - } +// // Make backup of existing file. +// err = CopyFile(filePath, filePath+backupExtension) +// if err != nil { +// return fmt.Errorf( +// "failed to create backup of %s from %s to %s: %w", +// name, +// filePath, +// filePath+backupExtension, +// err, +// ) +// } - // Open destination file for writing. - atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) - if err != nil { - return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) - } - defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway +// // Open destination file for writing. +// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) +// // if err != nil { +// // return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) +// // } +// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - // Write file. - _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) - if err != nil { - return err - } +// // // Write file. +// // _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) +// // if err != nil { +// // return err +// // } - // Finalize file. - err = atomicDstFile.CloseAtomicallyReplace() - if err != nil { - return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) - } +// // // Finalize file. +// // err = atomicDstFile.CloseAtomicallyReplace() +// // if err != nil { +// // return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) +// // } - log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) - return nil -} +// log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) +// return nil +// } -// upgradeSystemIntegrationFilePermissions upgrades the file permissions. -// System integration files are not necessarily present and may also be -// edited by third parties, such as the OS itself or other installers. -func upgradeSystemIntegrationFilePermissions( - name string, - filePath string, - fileMode fs.FileMode, -) error { - // Get current file permissions. - stat, err := os.Stat(filePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) - } +// // upgradeSystemIntegrationFilePermissions upgrades the file permissions. +// // System integration files are not necessarily present and may also be +// // edited by third parties, such as the OS itself or other installers. +// func upgradeSystemIntegrationFilePermissions( +// name string, +// filePath string, +// fileMode fs.FileMode, +// ) error { +// // Get current file permissions. +// stat, err := os.Stat(filePath) +// if err != nil { +// if errors.Is(err, os.ErrNotExist) { +// return nil +// } +// return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) +// } - // If permissions are as expected, do nothing. - if stat.Mode().Perm() == fileMode { - return nil - } +// // If permissions are as expected, do nothing. +// if stat.Mode().Perm() == fileMode { +// return nil +// } - // Otherwise, set correct permissions. - err = os.Chmod(filePath, fileMode) - if err != nil { - return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) - } +// // Otherwise, set correct permissions. +// err = os.Chmod(filePath, fileMode) +// if err != nil { +// return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) +// } - log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) - return nil -} +// log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) +// return nil +// } diff --git a/service/updates/registry.go b/service/updates/registry.go deleted file mode 100644 index de15a98b..00000000 --- a/service/updates/registry.go +++ /dev/null @@ -1 +0,0 @@ -package updates diff --git a/service/updates/bundle.go b/service/updates/registry/bundle.go similarity index 86% rename from service/updates/bundle.go rename to service/updates/registry/bundle.go index 5214db1b..3034438a 100644 --- a/service/updates/bundle.go +++ b/service/updates/registry/bundle.go @@ -1,4 +1,4 @@ -package updates +package registry import ( "archive/zip" @@ -17,6 +17,12 @@ import ( "github.com/safing/portmaster/base/log" ) +const ( + defaultFileMode = os.FileMode(0o0644) + executableFileMode = os.FileMode(0o0744) + defaultDirMode = os.FileMode(0o0755) +) + const MaxUnpackSize = 1 << 30 // 2^30 == 1GB type Artifact struct { @@ -29,40 +35,40 @@ type Artifact struct { } type Bundle struct { + dir string Name string `json:"Bundle"` Version string `json:"Version"` Published time.Time `json:"Published"` Artifacts []Artifact `json:"Artifacts"` } -func (bundle Bundle) downloadAndVerify(dataDir string) { +func (bundle Bundle) downloadAndVerify() { client := http.Client{} for _, artifact := range bundle.Artifacts { - filePath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + filePath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) // TODO(vladimir): is this needed? - _ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm) + _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode) // Check file is already downloaded and valid. - exists, err := checkIfFileIsValid(filePath, artifact) + exists, _ := checkIfFileIsValid(filePath, artifact) if exists { - log.Debugf("file already download: %s", filePath) + log.Debugf("updates: file already downloaded: %s", filePath) continue - } else if err != nil { - log.Errorf("error while checking old download: %s", err) } // Download artifact - err = processArtifact(&client, artifact, filePath) + err := processArtifact(&client, artifact, filePath) if err != nil { log.Errorf("updates: %s", err) } } } -func (bundle Bundle) Verify(dataDir string) error { +// Verify checks if the files are present int the dataDir and have the correct hash. +func (bundle Bundle) Verify() error { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) + artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) file, err := os.Open(artifactPath) if err != nil { return fmt.Errorf("failed to open file %s: %w", artifactPath, err) @@ -86,8 +92,7 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { // Check if file already exists file, err := os.Open(filename) if err != nil { - //nolint:nilerr - return false, nil + return false, err } defer func() { _ = file.Close() }() @@ -131,7 +136,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er // Verify hash := sha256.Sum256(content) if !bytes.Equal(providedHash, hash[:]) { - // FIXME(vladimir): just for testing. Make it an error before commit. + // FIXME(vladimir): just for testing. Make it an error. err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename) log.Debugf("updates: %s", err) } @@ -142,6 +147,11 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er if err != nil { return fmt.Errorf("failed to create file: %w", err) } + if artifact.Platform == "" { + _ = file.Chmod(defaultFileMode) + } else { + _ = file.Chmod(executableFileMode) + } _, err = file.Write(content) if err != nil { return fmt.Errorf("failed to write to file: %w", err) diff --git a/service/updates/registry/index.go b/service/updates/registry/index.go new file mode 100644 index 00000000..f5900b3f --- /dev/null +++ b/service/updates/registry/index.go @@ -0,0 +1,56 @@ +package registry + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/safing/portmaster/base/log" +) + +type UpdateIndex struct { + Directory string + DownloadDirectory string + Ignore []string + IndexURLs []string + IndexFile string + AutoApply bool +} + +func (ui *UpdateIndex) downloadIndexFile() (err error) { + _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) + for _, url := range ui.IndexURLs { + err = ui.downloadIndexFileFromURL(url) + if err != nil { + log.Warningf("updates: %s", err) + continue + } + // Downloading was successful. + err = nil + break + } + return +} + +func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { + client := http.Client{} + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed a get request to %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + return nil +} diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go new file mode 100644 index 00000000..223b90de --- /dev/null +++ b/service/updates/registry/registry.go @@ -0,0 +1,245 @@ +package registry + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/safing/portmaster/base/log" +) + +var ErrNotFound error = errors.New("file not found") + +type File struct { + id string + path string +} + +func (f *File) Identifier() string { + return f.id +} + +func (f *File) Path() string { + return f.path +} + +func (f *File) Version() string { + return "" +} + +type Registry struct { + binaryUpdateIndex UpdateIndex + intelUpdateIndex UpdateIndex + + binaryBundle *Bundle + intelBundle *Bundle + + binaryUpdateBundle *Bundle + intelUpdateBundle *Bundle + + files map[string]File +} + +// New create new Registry. +func New(binIndex UpdateIndex, intelIndex UpdateIndex) Registry { + return Registry{ + binaryUpdateIndex: binIndex, + intelUpdateIndex: intelIndex, + files: make(map[string]File), + } +} + +// Initialize parses and initializes currently installed bundles. +func (reg *Registry) Initialize() error { + var err error + + // Parse current installed binary bundle. + reg.binaryBundle, err = parseBundle(reg.binaryUpdateIndex.Directory, reg.binaryUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse binary bundle: %w", err) + } + // Parse current installed intel bundle. + reg.intelBundle, err = parseBundle(reg.intelUpdateIndex.Directory, reg.intelUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse intel bundle: %w", err) + } + + // Add bundle artifacts to registry. + reg.processBundle(reg.binaryBundle) + reg.processBundle(reg.intelBundle) + + return nil +} + +func (reg *Registry) processBundle(bundle *Bundle) { + for _, artifact := range bundle.Artifacts { + artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath} + } +} + +// GetFile returns the object of a artifact by id. +func (reg *Registry) GetFile(id string) (*File, error) { + file, ok := reg.files[id] + if ok { + return &file, nil + } else { + log.Errorf("updates: requested file id not found: %s", id) + return nil, ErrNotFound + } +} + +// CheckForBinaryUpdates checks if there is a new binary bundle updates. +func (reg *Registry) CheckForBinaryUpdates() (bool, error) { + err := reg.binaryUpdateIndex.downloadIndexFile() + if err != nil { + return false, err + } + + reg.binaryUpdateBundle, err = parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + if err != nil { + return false, fmt.Errorf("failed to parse bundle file: %w", err) + } + + // TODO(vladimir): Make a better check. + if reg.binaryBundle.Version != reg.binaryUpdateBundle.Version { + return true, nil + } + + return false, nil +} + +// DownloadBinaryUpdates downloads available binary updates. +func (reg *Registry) DownloadBinaryUpdates() error { + if reg.binaryUpdateBundle == nil { + // CheckForBinaryUpdates needs to be called before this. + return fmt.Errorf("no valid update bundle found") + } + _ = deleteUnfinishedDownloads(reg.binaryBundle.dir) + reg.binaryUpdateBundle.downloadAndVerify() + return nil +} + +// CheckForIntelUpdates checks if there is a new intel data bundle updates. +func (reg *Registry) CheckForIntelUpdates() (bool, error) { + err := reg.intelUpdateIndex.downloadIndexFile() + if err != nil { + return false, err + } + + reg.intelUpdateBundle, err = parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) + if err != nil { + return false, fmt.Errorf("failed to parse bundle file: %w", err) + } + + // TODO(vladimir): Make a better check. + if reg.intelBundle.Version != reg.intelUpdateBundle.Version { + return true, nil + } + + return false, nil +} + +// DownloadIntelUpdates downloads available intel data updates. +func (reg *Registry) DownloadIntelUpdates() error { + if reg.intelUpdateBundle == nil { + // CheckForIntelUpdates needs to be called before this. + return fmt.Errorf("no valid update bundle found") + } + _ = deleteUnfinishedDownloads(reg.intelBundle.dir) + reg.intelUpdateBundle.downloadAndVerify() + return nil +} + +// ApplyBinaryUpdates removes the current binary folder and replaces it with the downloaded one. +func (reg *Registry) ApplyBinaryUpdates() error { + bundle, err := parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse index file: %w", err) + } + err = bundle.Verify() + if err != nil { + return fmt.Errorf("binary bundle is not valid: %w", err) + } + + err = os.RemoveAll(reg.binaryUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to remove dir: %w", err) + } + err = os.Rename(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to move dir: %w", err) + } + return nil +} + +// ApplyIntelUpdates removes the current intel folder and replaces it with the downloaded one. +func (reg *Registry) ApplyIntelUpdates() error { + bundle, err := parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) + if err != nil { + return fmt.Errorf("failed to parse index file: %w", err) + } + err = bundle.Verify() + if err != nil { + return fmt.Errorf("binary bundle is not valid: %w", err) + } + + err = os.RemoveAll(reg.intelUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to remove dir: %w", err) + } + err = os.Rename(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.Directory) + if err != nil { + return fmt.Errorf("failed to move dir: %w", err) + } + return nil +} + +func parseBundle(dir string, indexFile string) (*Bundle, error) { + filepath := fmt.Sprintf("%s/%s", dir, indexFile) + // Check if the file exists. + file, err := os.Open(filepath) + if err != nil { + return nil, fmt.Errorf("failed to open index file: %w", err) + } + defer func() { _ = file.Close() }() + + // Read + content, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + // Parse + var bundle Bundle + err = json.Unmarshal(content, &bundle) + if err != nil { + return nil, err + } + bundle.dir = dir + + return &bundle, nil +} + +func deleteUnfinishedDownloads(rootDir string) error { + return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Check if the current file has the specified extension + if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { + log.Warningf("updates deleting unfinished: %s\n", path) + err := os.Remove(path) + if err != nil { + return fmt.Errorf("failed to delete file %s: %w", path, err) + } + } + + return nil + }) +} diff --git a/service/updates/restart.go b/service/updates/restart.go index 729853ff..30fc9289 100644 --- a/service/updates/restart.go +++ b/service/updates/restart.go @@ -54,7 +54,7 @@ func DelayedRestart(delay time.Duration) { // Schedule the restart task. log.Warningf("updates: restart triggered, will execute in %s", delay) restartAt := time.Now().Add(delay) - module.restartWorkerMgr.Delay(delay) + // module.restartWorkerMgr.Delay(delay) // Set restartTime. restartTimeLock.Lock() @@ -68,23 +68,23 @@ func AbortRestart() { log.Warningf("updates: restart aborted") // Cancel schedule. - module.restartWorkerMgr.Delay(0) + // module.restartWorkerMgr.Delay(0) } } // TriggerRestartIfPending triggers an automatic restart, if one is pending. // This can be used to prepone a scheduled restart if the conditions are preferable. func TriggerRestartIfPending() { - if restartPending.IsSet() { - module.restartWorkerMgr.Go() - } + // if restartPending.IsSet() { + // module.restartWorkerMgr.Go() + // } } // RestartNow immediately executes a restart. // This only works if the process is managed by portmaster-start. func RestartNow() { restartPending.Set() - module.restartWorkerMgr.Go() + // module.restartWorkerMgr.Go() } func automaticRestart(w *mgr.WorkerCtx) error { @@ -108,11 +108,11 @@ func automaticRestart(w *mgr.WorkerCtx) error { } // Set restart exit code. - if !rebooting { - module.instance.Restart() - } else { - module.instance.Shutdown() - } + // if !rebooting { + // module.instance.Restart() + // } else { + // module.instance.Shutdown() + // } } return nil diff --git a/service/updates/state.go b/service/updates/state.go index 3a1144b1..ef104308 100644 --- a/service/updates/state.go +++ b/service/updates/state.go @@ -1,49 +1,49 @@ package updates -import ( - "github.com/safing/portmaster/base/database/record" - "github.com/safing/portmaster/base/runtime" - "github.com/safing/portmaster/base/updater" -) +// import ( +// "github.com/safing/portmaster/base/database/record" +// "github.com/safing/portmaster/base/runtime" +// "github.com/safing/portmaster/base/updater" +// ) -var pushRegistryStatusUpdate runtime.PushFunc +// var pushRegistryStatusUpdate runtime.PushFunc -// RegistryStateExport is a wrapper to export the registry state. -type RegistryStateExport struct { - record.Base - *updater.RegistryState -} +// // RegistryStateExport is a wrapper to export the registry state. +// type RegistryStateExport struct { +// record.Base +// *updater.RegistryState +// } -func exportRegistryState(s *updater.RegistryState) *RegistryStateExport { - if s == nil { - state := registry.GetState() - s = &state - } +// func exportRegistryState(s *updater.RegistryState) *RegistryStateExport { +// // if s == nil { +// // state := registry.GetState() +// // s = &state +// // } - export := &RegistryStateExport{ - RegistryState: s, - } +// export := &RegistryStateExport{ +// RegistryState: s, +// } - export.CreateMeta() - export.SetKey("runtime:core/updates/state") +// export.CreateMeta() +// export.SetKey("runtime:core/updates/state") - return export -} +// return export +// } -func pushRegistryState(s *updater.RegistryState) { - export := exportRegistryState(s) - pushRegistryStatusUpdate(export) -} +// func pushRegistryState(s *updater.RegistryState) { +// export := exportRegistryState(s) +// pushRegistryStatusUpdate(export) +// } -func registerRegistryStateProvider() (err error) { - registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { - return []record.Record{exportRegistryState(nil)}, nil - }) +// func registerRegistryStateProvider() (err error) { +// registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { +// return []record.Record{exportRegistryState(nil)}, nil +// }) - pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider) - if err != nil { - return err - } +// pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider) +// if err != nil { +// return err +// } - return nil -} +// return nil +// } diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go index e4685c3b..334ebe88 100644 --- a/service/updates/upgrader.go +++ b/service/updates/upgrader.go @@ -1,406 +1,403 @@ package updates -import ( - "context" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" +// import ( +// "context" +// "fmt" +// "os" +// "os/exec" +// "path/filepath" +// "regexp" +// "strings" +// "time" - processInfo "github.com/shirou/gopsutil/process" - "github.com/tevino/abool" +// processInfo "github.com/shirou/gopsutil/process" +// "github.com/tevino/abool" - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/notifications" - "github.com/safing/portmaster/base/rng" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils/renameio" - "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates/helper" -) +// "github.com/safing/portmaster/base/dataroot" +// "github.com/safing/portmaster/base/info" +// "github.com/safing/portmaster/base/log" +// "github.com/safing/portmaster/base/notifications" +// "github.com/safing/portmaster/base/rng" +// "github.com/safing/portmaster/base/updater" +// "github.com/safing/portmaster/service/mgr" +// ) -const ( - upgradedSuffix = "-upgraded" - exeExt = ".exe" -) +// const ( +// upgradedSuffix = "-upgraded" +// exeExt = ".exe" +// ) -var ( - upgraderActive = abool.NewBool(false) +// var ( +// upgraderActive = abool.NewBool(false) - pmCtrlUpdate *updater.File - pmCoreUpdate *updater.File +// pmCtrlUpdate *updater.File +// pmCoreUpdate *updater.File - spnHubUpdate *updater.File +// spnHubUpdate *updater.File - rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) -) +// rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) +// ) -func initUpgrader() error { - module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) - return nil -} +// func initUpgrader() error { +// // module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) +// return nil +// } -func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { - // Lock runs, but discard additional runs. - if !upgraderActive.SetToIf(false, true) { - return false, nil - } - defer upgraderActive.SetTo(false) +// func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { +// // Lock runs, but discard additional runs. +// if !upgraderActive.SetToIf(false, true) { +// return false, nil +// } +// defer upgraderActive.SetTo(false) - // Upgrade portmaster-start. - err = upgradePortmasterStart() - if err != nil { - log.Warningf("updates: failed to upgrade portmaster-start: %s", err) - } +// // Upgrade portmaster-start. +// err = upgradePortmasterStart() +// if err != nil { +// log.Warningf("updates: failed to upgrade portmaster-start: %s", err) +// } - // Upgrade based on binary. - binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] - switch binBaseName { - case "portmaster-core": - // Notify about upgrade. - if err := upgradeCoreNotify(); err != nil { - log.Warningf("updates: failed to notify about core upgrade: %s", err) - } +// // Upgrade based on binary. +// binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] +// switch binBaseName { +// case "portmaster-core": +// // Notify about upgrade. +// if err := upgradeCoreNotify(); err != nil { +// log.Warningf("updates: failed to notify about core upgrade: %s", err) +// } - // Fix chrome sandbox permissions. - if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { - log.Warningf("updates: failed to handle electron upgrade: %s", err) - } +// // Fix chrome sandbox permissions. +// // if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { +// // log.Warningf("updates: failed to handle electron upgrade: %s", err) +// // } - // Upgrade system integration. - upgradeSystemIntegration() +// // Upgrade system integration. +// upgradeSystemIntegration() - case "spn-hub": - // Trigger upgrade procedure. - if err := upgradeHub(); err != nil { - log.Warningf("updates: failed to initiate hub upgrade: %s", err) - } - } +// case "spn-hub": +// // Trigger upgrade procedure. +// if err := upgradeHub(); err != nil { +// log.Warningf("updates: failed to initiate hub upgrade: %s", err) +// } +// } - return false, nil -} +// return false, nil +// } -func upgradeCoreNotify() error { - if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { - return nil - } +// func upgradeCoreNotify() error { +// if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { +// return nil +// } - // make identifier - identifier := "core/portmaster-core" // identifier, use forward slash! - if onWindows { - identifier += exeExt - } +// // make identifier +// identifier := "core/portmaster-core" // identifier, use forward slash! +// if onWindows { +// identifier += exeExt +// } - // get newest portmaster-core - newFile, err := GetPlatformFile(identifier) - if err != nil { - return err - } - pmCoreUpdate = newFile +// // get newest portmaster-core +// // newFile, err := GetPlatformFile(identifier) +// // if err != nil { +// // return err +// // } +// // pmCoreUpdate = newFile - // check for new version - if info.VersionNumber() != pmCoreUpdate.Version() { - n := notifications.Notify(¬ifications.Notification{ - EventID: "updates:core-update-available", - Type: notifications.Info, - Title: fmt.Sprintf( - "Portmaster Update v%s Is Ready!", - pmCoreUpdate.Version(), - ), - Category: "Core", - Message: fmt.Sprintf( - `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, - pmCoreUpdate.Version(), - ), - ShowOnSystem: true, - AvailableActions: []*notifications.Action{ - // TODO: Use special UI action in order to reload UI on restart. - { - ID: "restart", - Text: "Restart", - }, - { - ID: "later", - Text: "Not now", - }, - }, - }) - n.SetActionFunction(upgradeCoreNotifyActionHandler) +// // check for new version +// if info.VersionNumber() != pmCoreUpdate.Version() { +// n := notifications.Notify(¬ifications.Notification{ +// EventID: "updates:core-update-available", +// Type: notifications.Info, +// Title: fmt.Sprintf( +// "Portmaster Update v%s Is Ready!", +// pmCoreUpdate.Version(), +// ), +// Category: "Core", +// Message: fmt.Sprintf( +// `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, +// pmCoreUpdate.Version(), +// ), +// ShowOnSystem: true, +// AvailableActions: []*notifications.Action{ +// // TODO: Use special UI action in order to reload UI on restart. +// { +// ID: "restart", +// Text: "Restart", +// }, +// { +// ID: "later", +// Text: "Not now", +// }, +// }, +// }) +// n.SetActionFunction(upgradeCoreNotifyActionHandler) - log.Debugf("updates: new portmaster version available, sending notification to user") - } +// log.Debugf("updates: new portmaster version available, sending notification to user") +// } - return nil -} +// return nil +// } -func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { - switch n.SelectedActionID { - case "restart": - log.Infof("updates: user triggered restart via core update notification") - RestartNow() - case "later": - n.Delete() - } +// func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { +// switch n.SelectedActionID { +// case "restart": +// log.Infof("updates: user triggered restart via core update notification") +// RestartNow() +// case "later": +// n.Delete() +// } - return nil -} +// return nil +// } -func upgradeHub() error { - if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { - return nil - } +// func upgradeHub() error { +// if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { +// return nil +// } - // Make identifier for getting file from updater. - identifier := "hub/spn-hub" // identifier, use forward slash! - if onWindows { - identifier += exeExt - } +// // Make identifier for getting file from updater. +// identifier := "hub/spn-hub" // identifier, use forward slash! +// if onWindows { +// identifier += exeExt +// } - // Get newest spn-hub file. - newFile, err := GetPlatformFile(identifier) - if err != nil { - return err - } - spnHubUpdate = newFile +// // Get newest spn-hub file. +// // newFile, err := GetPlatformFile(identifier) +// // if err != nil { +// // return err +// // } +// // spnHubUpdate = newFile - // Check if the new version is different. - if info.GetInfo().Version != spnHubUpdate.Version() { - // Get random delay with up to three hours. - delayMinutes, err := rng.Number(3 * 60) - if err != nil { - return err - } +// // Check if the new version is different. +// if info.GetInfo().Version != spnHubUpdate.Version() { +// // Get random delay with up to three hours. +// delayMinutes, err := rng.Number(3 * 60) +// if err != nil { +// return err +// } - // Delay restart for at least one hour for preparations. - DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) +// // Delay restart for at least one hour for preparations. +// DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) - // Increase update checks in order to detect aborts better. - // if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(10 * time.Minute) - // } - } else { - AbortRestart() +// // Increase update checks in order to detect aborts better. +// // if !disableTaskSchedule { +// // module.updateBinaryWorkerMgr.Repeat(10 * time.Minute) +// // } +// } else { +// AbortRestart() - // Set update task schedule back to normal. - // if !disableTaskSchedule { - module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) - // } - } +// // Set update task schedule back to normal. +// // if !disableTaskSchedule { +// // module.updateBinaryWorkerMgr.Repeat(updateTaskRepeatDuration) +// // } +// } - return nil -} +// return nil +// } -func upgradePortmasterStart() error { - filename := "portmaster-start" - if onWindows { - filename += exeExt - } +// func upgradePortmasterStart() error { +// filename := "portmaster-start" +// if onWindows { +// filename += exeExt +// } - // check if we can upgrade - if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { - // get newest portmaster-start - newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! - if err != nil { - return err - } - pmCtrlUpdate = newFile - } else { - return nil - } +// // check if we can upgrade +// if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { +// // get newest portmaster-start +// // newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! +// // if err != nil { +// // return err +// // } +// // pmCtrlUpdate = newFile +// } else { +// return nil +// } - // update portmaster-start in data root - rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) - err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) - if err != nil { - return err - } +// // update portmaster-start in data root +// rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) +// err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) +// if err != nil { +// return err +// } - return nil -} +// return nil +// } -func warnOnIncorrectParentPath() { - expectedFileName := "portmaster-start" - if onWindows { - expectedFileName += exeExt - } +// func warnOnIncorrectParentPath() { +// expectedFileName := "portmaster-start" +// if onWindows { +// expectedFileName += exeExt +// } - // upgrade parent process, if it's portmaster-start - parent, err := processInfo.NewProcess(int32(os.Getppid())) - if err != nil { - log.Tracef("could not get parent process: %s", err) - return - } - parentName, err := parent.Name() - if err != nil { - log.Tracef("could not get parent process name: %s", err) - return - } - if parentName != expectedFileName { - // Only warn about this if not in dev mode. - if !devMode() { - log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) - } +// // upgrade parent process, if it's portmaster-start +// parent, err := processInfo.NewProcess(int32(os.Getppid())) +// if err != nil { +// log.Tracef("could not get parent process: %s", err) +// return +// } +// parentName, err := parent.Name() +// if err != nil { +// log.Tracef("could not get parent process name: %s", err) +// return +// } +// if parentName != expectedFileName { +// // Only warn about this if not in dev mode. +// if !devMode() { +// log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) +// } - // TODO(ppacher): once we released a new installer and folks had time - // to update we should send a module warning/hint to the - // UI notifying the user that he's still using portmaster-control. - return - } +// // TODO(ppacher): once we released a new installer and folks had time +// // to update we should send a module warning/hint to the +// // UI notifying the user that he's still using portmaster-control. +// return +// } - parentPath, err := parent.Exe() - if err != nil { - log.Tracef("could not get parent process path: %s", err) - return - } +// // parentPath, err := parent.Exe() +// // if err != nil { +// // log.Tracef("could not get parent process path: %s", err) +// // return +// // } - absPath, err := filepath.Abs(parentPath) - if err != nil { - log.Tracef("could not get absolut parent process path: %s", err) - return - } +// // absPath, err := filepath.Abs(parentPath) +// // if err != nil { +// // log.Tracef("could not get absolut parent process path: %s", err) +// // return +// // } - root := filepath.Dir(registry.StorageDir().Path) - if !strings.HasPrefix(absPath, root) { - log.Warningf("detected unexpected path %s for portmaster-start", absPath) - notifications.NotifyWarn( - "updates:unsupported-parent", - "Unsupported Launcher", - fmt.Sprintf( - "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", - expectedFileName, - absPath, - filepath.Join(root, expectedFileName), - ), - ) - } -} +// // root := filepath.Dir(registry.StorageDir().Path) +// // if !strings.HasPrefix(absPath, root) { +// // log.Warningf("detected unexpected path %s for portmaster-start", absPath) +// // notifications.NotifyWarn( +// // "updates:unsupported-parent", +// // "Unsupported Launcher", +// // fmt.Sprintf( +// // "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", +// // expectedFileName, +// // absPath, +// // filepath.Join(root, expectedFileName), +// // ), +// // ) +// // } +// } -func upgradeBinary(fileToUpgrade string, file *updater.File) error { - fileExists := false - _, err := os.Stat(fileToUpgrade) - if err == nil { - // file exists and is accessible - fileExists = true - } +// func upgradeBinary(fileToUpgrade string, file *updater.File) error { +// fileExists := false +// _, err := os.Stat(fileToUpgrade) +// if err == nil { +// // file exists and is accessible +// fileExists = true +// } - if fileExists { - // get current version - var currentVersion string - cmd := exec.Command(fileToUpgrade, "version", "--short") - out, err := cmd.Output() - if err == nil { - // abort if version matches - currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") - if currentVersion == file.Version() { - log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) - // already up to date! - return nil - } - } else { - log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) - currentVersion = "0.0.0" - } +// if fileExists { +// // get current version +// var currentVersion string +// cmd := exec.Command(fileToUpgrade, "version", "--short") +// out, err := cmd.Output() +// if err == nil { +// // abort if version matches +// currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") +// if currentVersion == file.Version() { +// log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) +// // already up to date! +// return nil +// } +// } else { +// log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) +// currentVersion = "0.0.0" +// } - // test currentVersion for sanity - if !rawVersionRegex.MatchString(currentVersion) { - log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) - } +// // test currentVersion for sanity +// if !rawVersionRegex.MatchString(currentVersion) { +// log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) +// } - // try removing old version - err = os.Remove(fileToUpgrade) - if err != nil { - // ensure tmp dir is here - err = registry.TmpDir().Ensure() - if err != nil { - return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) - } +// // try removing old version +// err = os.Remove(fileToUpgrade) +// if err != nil { +// // ensure tmp dir is here +// // err = registry.TmpDir().Ensure() +// // if err != nil { +// // return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) +// // } - // maybe we're on windows and it's in use, try moving - err = os.Rename(fileToUpgrade, filepath.Join( - registry.TmpDir().Path, - fmt.Sprintf( - "%s-%d%s", - filepath.Base(fileToUpgrade), - time.Now().UTC().Unix(), - upgradedSuffix, - ), - )) - if err != nil { - return fmt.Errorf("unable to move file that needs upgrade: %w", err) - } - } - } +// // maybe we're on windows and it's in use, try moving +// // err = os.Rename(fileToUpgrade, filepath.Join( +// // registry.TmpDir().Path, +// // fmt.Sprintf( +// // "%s-%d%s", +// // filepath.Base(fileToUpgrade), +// // time.Now().UTC().Unix(), +// // upgradedSuffix, +// // ), +// // )) +// // if err != nil { +// // return fmt.Errorf("unable to move file that needs upgrade: %w", err) +// // } +// } +// } - // copy upgrade - err = CopyFile(file.Path(), fileToUpgrade) - if err != nil { - // try again - time.Sleep(1 * time.Second) - err = CopyFile(file.Path(), fileToUpgrade) - if err != nil { - return err - } - } +// // copy upgrade +// err = CopyFile(file.Path(), fileToUpgrade) +// if err != nil { +// // try again +// time.Sleep(1 * time.Second) +// err = CopyFile(file.Path(), fileToUpgrade) +// if err != nil { +// return err +// } +// } - // check permissions - if !onWindows { - info, err := os.Stat(fileToUpgrade) - if err != nil { - return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) - } - if info.Mode() != 0o0755 { - err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. - if err != nil { - return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) - } - } - } +// // check permissions +// if !onWindows { +// info, err := os.Stat(fileToUpgrade) +// if err != nil { +// return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) +// } +// if info.Mode() != 0o0755 { +// err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. +// if err != nil { +// return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) +// } +// } +// } - log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) - return nil -} +// log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) +// return nil +// } -// CopyFile atomically copies a file using the update registry's tmp dir. -func CopyFile(srcPath, dstPath string) error { - // check tmp dir - err := registry.TmpDir().Ensure() - if err != nil { - return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) - } +// // CopyFile atomically copies a file using the update registry's tmp dir. +// func CopyFile(srcPath, dstPath string) error { +// // check tmp dir +// // err := registry.TmpDir().Ensure() +// // if err != nil { +// // return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) +// // } - // open file for writing - atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) - if err != nil { - return fmt.Errorf("could not create temp file for atomic copy: %w", err) - } - defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway +// // open file for writing +// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) +// // if err != nil { +// // return fmt.Errorf("could not create temp file for atomic copy: %w", err) +// // } +// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - // open source - srcFile, err := os.Open(srcPath) - if err != nil { - return err - } - defer func() { - _ = srcFile.Close() - }() +// // // open source +// // srcFile, err := os.Open(srcPath) +// // if err != nil { +// // return err +// // } +// // defer func() { +// // _ = srcFile.Close() +// // }() - // copy data - _, err = io.Copy(atomicDstFile, srcFile) - if err != nil { - return err - } +// // // copy data +// // _, err = io.Copy(atomicDstFile, srcFile) +// // if err != nil { +// // return err +// // } - // finalize file - err = atomicDstFile.CloseAtomicallyReplace() - if err != nil { - return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) - } +// // // finalize file +// // err = atomicDstFile.CloseAtomicallyReplace() +// // if err != nil { +// // return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) +// // } - return nil -} +// return nil +// } diff --git a/spn/captain/intel.go b/spn/captain/intel.go index c71ce663..6411f4c6 100644 --- a/spn/captain/intel.go +++ b/spn/captain/intel.go @@ -6,9 +6,8 @@ import ( "os" "sync" - "github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" "github.com/safing/portmaster/spn/conf" "github.com/safing/portmaster/spn/hub" "github.com/safing/portmaster/spn/navigator" @@ -16,7 +15,7 @@ import ( ) var ( - intelResource *updater.File + intelResource *registry.File intelResourcePath = "intel/spn/main-intel.yaml" intelResourceMapName = "main" intelResourceUpdateLock sync.Mutex @@ -44,12 +43,13 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { } // Check if there is something to do. - if intelResource != nil && !intelResource.UpgradeAvailable() { + // TODO(vladimir): is update check needed + if intelResource != nil { //&& !intelResource.UpgradeAvailable() { return nil } // Get intel file and load it from disk. - intelResource, err = updates.GetFile(intelResourcePath) + intelResource, err = module.instance.Updates().GetFile(intelResourcePath) if err != nil { return fmt.Errorf("failed to get SPN intel update: %w", err) } From f7abb700bf13e48652f2722fc7bb1d57b613b976 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 27 Aug 2024 14:30:04 +0300 Subject: [PATCH 07/62] [WIP] Fix SELinux permissions --- packaging/linux/portmaster.service | 5 +++-- packaging/linux/postinst | 11 +++++++++++ packaging/linux/postrm | 9 +++++++++ service/updates/module.go | 6 +++--- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service index 5490ac6f..c16068d9 100644 --- a/packaging/linux/portmaster.service +++ b/packaging/linux/portmaster.service @@ -34,8 +34,9 @@ AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_ne CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon StateDirectory=portmaster # TODO(ppacher): add --disable-software-updates once it's merged and the release process changed. -ExecStart=/usr/bin/portmaster-core --data /opt/safing/portmaster -- $PORTMASTER_ARGS -ExecStopPost=-/usr/bin/portmaster-core recover-iptables +WorkingDirectory=/var/lib/portmaster/data +ExecStart=/usr/lib/portmaster/portmaster-core --data /var/lib/portmaster/data -devmode -- $PORTMASTER_ARGS +ExecStopPost=-/usr/bin/portmaster/portmaster-core recover-iptables [Install] WantedBy=multi-user.target diff --git a/packaging/linux/postinst b/packaging/linux/postinst index 8f727403..ab056723 100644 --- a/packaging/linux/postinst +++ b/packaging/linux/postinst @@ -1,5 +1,16 @@ #!/bin/bash +chmod +x /usr/lib/portmaster/portmaster-core + +# +# Fix selinux permissions for portmaster-core if we have semanage +# available. +# +if command -V semanage >/dev/null 2>&1; then + semanage fcontext -a -t bin_t -s system_u $(realpath /usr/lib)'/portmaster/portmaster-core' || : + restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || : +fi + systemctl daemon-reload systemctl enable portmaster.service diff --git a/packaging/linux/postrm b/packaging/linux/postrm index a9bf588e..9dddf8e9 100644 --- a/packaging/linux/postrm +++ b/packaging/linux/postrm @@ -1 +1,10 @@ #!/bin/bash + +# +# Remove selinux permissions for portmaster-core if we have semanage +# available. +# +if command -V semanage >/dev/null 2>&1; then + semanage fcontext --delete $(realpath /usr/lib)'/portmaster/portmaster-core' || : + restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || : +fi \ No newline at end of file diff --git a/service/updates/module.go b/service/updates/module.go index 989ad020..31de626c 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -63,7 +63,7 @@ func New(instance instance) (*Updates, error) { binIndex := registry.UpdateIndex{ Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/portmaster/new_bin", + DownloadDirectory: "/var/lib/portmaster/new_bin", Ignore: []string{"databases", "intel", "config.json"}, IndexURLs: []string{"http://localhost:8000/test-binary.json"}, IndexFile: "bin-index.json", @@ -71,8 +71,8 @@ func New(instance instance) (*Updates, error) { } intelIndex := registry.UpdateIndex{ - Directory: "/var/portmaster/intel", - DownloadDirectory: "/var/portmaster/new_intel", + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, From 701505ae7535475cf9a84c7794e4200ae6fb7bb9 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 30 Aug 2024 12:40:51 +0300 Subject: [PATCH 08/62] [WIP] working download and replace. --- service/broadcasts/module.go | 2 +- service/broadcasts/notify.go | 2 +- service/instance.go | 77 +++++++++----- service/intel/filterlists/index.go | 2 +- service/intel/filterlists/module.go | 5 +- service/intel/geoip/database.go | 2 +- service/intel/geoip/module.go | 4 +- service/netenv/main.go | 2 +- service/netenv/online-status.go | 2 +- service/ui/module.go | 2 +- service/ui/serve.go | 2 +- service/updates/module.go | 115 ++++++-------------- service/updates/registry/bundle.go | 15 +-- service/updates/registry/index.go | 5 +- service/updates/registry/registry.go | 151 ++++++++++----------------- service/updates/restart.go | 50 --------- spn/captain/intel.go | 4 +- spn/captain/module.go | 2 +- 18 files changed, 168 insertions(+), 276 deletions(-) diff --git a/service/broadcasts/module.go b/service/broadcasts/module.go index 2d99115b..19c1641f 100644 --- a/service/broadcasts/module.go +++ b/service/broadcasts/module.go @@ -93,5 +93,5 @@ func New(instance instance) (*Broadcasts, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/broadcasts/notify.go b/service/broadcasts/notify.go index 73b05f98..235a1bf0 100644 --- a/service/broadcasts/notify.go +++ b/service/broadcasts/notify.go @@ -67,7 +67,7 @@ type BroadcastNotification struct { func broadcastNotify(ctx *mgr.WorkerCtx) error { // Get broadcast notifications file, load it from disk and parse it. - broadcastsResource, err := module.instance.Updates().GetFile(broadcastsResourcePath) + broadcastsResource, err := module.instance.IntelUpdates().GetFile(broadcastsResourcePath) if err != nil { return fmt.Errorf("failed to get broadcast notifications update: %w", err) } diff --git a/service/instance.go b/service/instance.go index ad6e9dab..43c7562b 100644 --- a/service/instance.go +++ b/service/instance.go @@ -34,6 +34,7 @@ import ( "github.com/safing/portmaster/service/sync" "github.com/safing/portmaster/service/ui" "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/registry" "github.com/safing/portmaster/spn/access" "github.com/safing/portmaster/spn/cabin" "github.com/safing/portmaster/spn/captain" @@ -46,6 +47,23 @@ import ( "github.com/safing/portmaster/spn/terminal" ) +var binaryUpdateIndex = registry.UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/lib/portmaster/new_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, +} + +var intelUpdateIndex = registry.UpdateIndex{ + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, +} + // Instance is an instance of a Portmaster service. type Instance struct { ctx context.Context @@ -63,25 +81,26 @@ type Instance struct { rng *rng.Rng base *base.Base - core *core.Core - updates *updates.Updates - geoip *geoip.GeoIP - netenv *netenv.NetEnv - ui *ui.UI - profile *profile.ProfileModule - network *network.Network - netquery *netquery.NetQuery - firewall *firewall.Firewall - filterLists *filterlists.FilterLists - interception *interception.Interception - customlist *customlists.CustomList - status *status.Status - broadcasts *broadcasts.Broadcasts - compat *compat.Compat - nameserver *nameserver.NameServer - process *process.ProcessModule - resolver *resolver.ResolverModule - sync *sync.Sync + core *core.Core + binaryUpdates *updates.Updates + intelUpdates *updates.Updates + geoip *geoip.GeoIP + netenv *netenv.NetEnv + ui *ui.UI + profile *profile.ProfileModule + network *network.Network + netquery *netquery.NetQuery + firewall *firewall.Firewall + filterLists *filterlists.FilterLists + interception *interception.Interception + customlist *customlists.CustomList + status *status.Status + broadcasts *broadcasts.Broadcasts + compat *compat.Compat + nameserver *nameserver.NameServer + process *process.ProcessModule + resolver *resolver.ResolverModule + sync *sync.Sync access *access.Access @@ -147,7 +166,11 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx if err != nil { return instance, fmt.Errorf("create core module: %w", err) } - instance.updates, err = updates.New(instance) + instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex) + if err != nil { + return instance, fmt.Errorf("create updates module: %w", err) + } + instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex) if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } @@ -274,7 +297,8 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx instance.notifications, instance.core, - instance.updates, + instance.binaryUpdates, + instance.intelUpdates, instance.geoip, instance.netenv, @@ -373,9 +397,14 @@ func (i *Instance) Base() *base.Base { return i.base } -// Updates returns the updates module. -func (i *Instance) Updates() *updates.Updates { - return i.updates +// BinaryUpdates returns the updates module. +func (i *Instance) BinaryUpdates() *updates.Updates { + return i.binaryUpdates +} + +// IntelUpdates returns the updates module. +func (i *Instance) IntelUpdates() *updates.Updates { + return i.intelUpdates } // GeoIP returns the geoip module. diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index 842a96b2..ba5267c2 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -175,7 +175,7 @@ func updateListIndex() error { case listIndexUpdate == nil: // This is the first time this function is run, get updater file for index. var err error - listIndexUpdate, err = module.instance.Updates().GetFile(listIndexFilePath) + listIndexUpdate, err = module.instance.IntelUpdates().GetFile(listIndexFilePath) if err != nil { return err } diff --git a/service/intel/filterlists/module.go b/service/intel/filterlists/module.go index 92f6576e..ecded5dc 100644 --- a/service/intel/filterlists/module.go +++ b/service/intel/filterlists/module.go @@ -57,11 +57,12 @@ func init() { } func prep() error { - module.instance.Updates().EventResourcesUpdated.AddCallback("Check for blocklist updates", + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("Check for blocklist updates", func(wc *mgr.WorkerCtx, s struct{}) (bool, error) { if ignoreUpdateEvents.IsSet() { return false, nil } + log.Debugf("performing filter list upadte") return false, tryListUpdate(wc.Ctx()) }) @@ -141,6 +142,6 @@ func New(instance instance) (*FilterLists, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates NetEnv() *netenv.NetEnv } diff --git a/service/intel/geoip/database.go b/service/intel/geoip/database.go index 72197cd8..99f02318 100644 --- a/service/intel/geoip/database.go +++ b/service/intel/geoip/database.go @@ -198,7 +198,7 @@ func getGeoIPDB(resource string) (*geoIPDB, error) { } func open(resource string) (*registry.File, error) { - f, err := module.instance.Updates().GetFile(resource) + f, err := module.instance.IntelUpdates().GetFile(resource) if err != nil { return nil, fmt.Errorf("getting file: %w", err) } diff --git a/service/intel/geoip/module.go b/service/intel/geoip/module.go index 6c2bb55e..2ebde990 100644 --- a/service/intel/geoip/module.go +++ b/service/intel/geoip/module.go @@ -19,7 +19,7 @@ func (g *GeoIP) Manager() *mgr.Manager { } func (g *GeoIP) Start() error { - module.instance.Updates().EventResourcesUpdated.AddCallback( + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback( "Check for GeoIP database updates", func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) { worker.triggerUpdate() @@ -66,5 +66,5 @@ func New(instance instance) (*GeoIP, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/netenv/main.go b/service/netenv/main.go index 20426772..81cd540f 100644 --- a/service/netenv/main.go +++ b/service/netenv/main.go @@ -107,5 +107,5 @@ func New(instance instance) (*NetEnv, error) { } type instance interface { - Updates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/netenv/online-status.go b/service/netenv/online-status.go index 137ce410..1d5feeaf 100644 --- a/service/netenv/online-status.go +++ b/service/netenv/online-status.go @@ -220,7 +220,7 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) // Trigger update check when coming (semi) online. if Online() { - module.instance.Updates().EventResourcesUpdated.Submit(struct{}{}) + module.instance.IntelUpdates().EventResourcesUpdated.Submit(struct{}{}) } } } diff --git a/service/ui/module.go b/service/ui/module.go index e9a8b324..2e4f1d9a 100644 --- a/service/ui/module.go +++ b/service/ui/module.go @@ -82,5 +82,5 @@ func New(instance instance) (*UI, error) { type instance interface { API() *api.API - Updates() *updates.Updates + BinaryUpdates() *updates.Updates } diff --git a/service/ui/serve.go b/service/ui/serve.go index 1455c3f6..ad41b2d5 100644 --- a/service/ui/serve.go +++ b/service/ui/serve.go @@ -91,7 +91,7 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // get file from update system - zipFile, err := module.instance.Updates().GetFile(fmt.Sprintf("%s.zip", moduleName)) + zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName)) if err != nil { if errors.Is(err, registry.ErrNotFound) { log.Tracef("ui: requested module %s does not exist", moduleName) diff --git a/service/updates/module.go b/service/updates/module.go index 31de626c..a4b6490c 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -1,10 +1,8 @@ package updates import ( - "errors" "flag" - "fmt" - "sync/atomic" + "time" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" @@ -14,10 +12,10 @@ import ( "github.com/safing/portmaster/service/updates/registry" ) -var applyUpdates bool +var autoUpdate bool func init() { - flag.BoolVar(&applyUpdates, "update", false, "apply downloaded updates") + flag.BoolVar(&autoUpdate, "auto-update", false, "auto apply downloaded updates") } // Updates provides access to released artifacts. @@ -25,9 +23,8 @@ type Updates struct { m *mgr.Manager states *mgr.StateMgr - updateBinaryWorkerMgr *mgr.WorkerMgr - updateIntelWorkerMgr *mgr.WorkerMgr - restartWorkerMgr *mgr.WorkerMgr + updateCheckWorkerMgr *mgr.WorkerMgr + upgraderWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] @@ -37,15 +34,9 @@ type Updates struct { instance instance } -var shimLoaded atomic.Bool - -// New returns a new UI module. -func New(instance instance) (*Updates, error) { - if !shimLoaded.CompareAndSwap(false, true) { - return nil, errors.New("only one instance allowed") - } - - m := mgr.New("Updates") +// New returns a new Updates module. +func New(instance instance, name string, index registry.UpdateIndex) (*Updates, error) { + m := mgr.New(name) module := &Updates{ m: m, states: m.NewStateMgr(), @@ -57,63 +48,47 @@ func New(instance instance) (*Updates, error) { } // Events - module.updateBinaryWorkerMgr = m.NewWorkerMgr("binary updater", module.checkForBinaryUpdates, nil) - module.updateIntelWorkerMgr = m.NewWorkerMgr("intel updater", module.checkForIntelUpdates, nil) - module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) + module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil) + module.updateCheckWorkerMgr.Repeat(30 * time.Second) + module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", func(w *mgr.WorkerCtx) error { + err := module.registry.ApplyUpdates() + if err != nil { + // TODO(vladimir): Send notification to UI + log.Errorf("updates: failed to apply updates: %s", err) + } else { + module.instance.Restart() + } + return nil + }, nil) - binIndex := registry.UpdateIndex{ - Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/lib/portmaster/new_bin", - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, - } - - intelIndex := registry.UpdateIndex{ - Directory: "/var/lib/portmaster/intel", - DownloadDirectory: "/var/lib/portmaster/new_intel", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, - } - module.registry = registry.New(binIndex, intelIndex) + module.registry = registry.New(index) + _ = module.registry.Initialize() return module, nil } -func (u *Updates) checkForBinaryUpdates(_ *mgr.WorkerCtx) error { - hasUpdates, err := u.registry.CheckForBinaryUpdates() +func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { + hasUpdates, err := u.registry.CheckForUpdates() if err != nil { - log.Errorf("updates: failed to check for binary updates: %s", err) + log.Errorf("updates: failed to check for updates: %s", err) } if hasUpdates { - log.Infof("updates: there is updates available in the binary bundle") - err = u.registry.DownloadBinaryUpdates() + log.Infof("updates: there is updates available") + err = u.registry.DownloadUpdates() if err != nil { log.Errorf("updates: failed to download bundle: %s", err) + } else if autoUpdate { + u.ApplyUpdates() } } else { - log.Infof("updates: no new binary updates") + log.Infof("updates: no new updates") + u.EventResourcesUpdated.Submit(struct{}{}) } return nil } -func (u *Updates) checkForIntelUpdates(_ *mgr.WorkerCtx) error { - hasUpdates, err := u.registry.CheckForIntelUpdates() - if err != nil { - log.Errorf("updates: failed to check for intel updates: %s", err) - } - if hasUpdates { - log.Infof("updates: there is updates available in the intel bundle") - err = u.registry.DownloadIntelUpdates() - if err != nil { - log.Errorf("updates: failed to download bundle: %s", err) - } - } else { - log.Infof("updates: no new intel data updates") - } - return nil +func (u *Updates) ApplyUpdates() { + u.upgraderWorkerMgr.Go() } // States returns the state manager. @@ -128,29 +103,7 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { - // initConfig() - - if applyUpdates { - err := u.registry.ApplyBinaryUpdates() - if err != nil { - log.Errorf("updates: failed to apply binary updates: %s", err) - } - err = u.registry.ApplyIntelUpdates() - if err != nil { - log.Errorf("updates: failed to apply intel updates: %s", err) - } - u.instance.Restart() - return nil - } - - err := u.registry.Initialize() - if err != nil { - // TODO(vladimir): Find a better way to handle this error. The service will stop if parsing of the bundle files fails. - return fmt.Errorf("failed to initialize registry: %w", err) - } - - u.updateBinaryWorkerMgr.Go() - u.updateIntelWorkerMgr.Go() + u.updateCheckWorkerMgr.Go() return nil } diff --git a/service/updates/registry/bundle.go b/service/updates/registry/bundle.go index 3034438a..24953966 100644 --- a/service/updates/registry/bundle.go +++ b/service/updates/registry/bundle.go @@ -17,12 +17,6 @@ import ( "github.com/safing/portmaster/base/log" ) -const ( - defaultFileMode = os.FileMode(0o0644) - executableFileMode = os.FileMode(0o0744) - defaultDirMode = os.FileMode(0o0755) -) - const MaxUnpackSize = 1 << 30 // 2^30 == 1GB type Artifact struct { @@ -35,18 +29,17 @@ type Artifact struct { } type Bundle struct { - dir string Name string `json:"Bundle"` Version string `json:"Version"` Published time.Time `json:"Published"` Artifacts []Artifact `json:"Artifacts"` } -func (bundle Bundle) downloadAndVerify() { +func (bundle Bundle) downloadAndVerify(dir string) { client := http.Client{} for _, artifact := range bundle.Artifacts { - filePath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + filePath := fmt.Sprintf("%s/%s", dir, artifact.Filename) // TODO(vladimir): is this needed? _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode) @@ -66,9 +59,9 @@ func (bundle Bundle) downloadAndVerify() { } // Verify checks if the files are present int the dataDir and have the correct hash. -func (bundle Bundle) Verify() error { +func (bundle Bundle) Verify(dir string) error { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + artifactPath := fmt.Sprintf("%s/%s", dir, artifact.Filename) file, err := os.Open(artifactPath) if err != nil { return fmt.Errorf("failed to open file %s: %w", artifactPath, err) diff --git a/service/updates/registry/index.go b/service/updates/registry/index.go index f5900b3f..cc237163 100644 --- a/service/updates/registry/index.go +++ b/service/updates/registry/index.go @@ -12,6 +12,7 @@ import ( type UpdateIndex struct { Directory string DownloadDirectory string + PurgeDirectory string Ignore []string IndexURLs []string IndexFile string @@ -23,7 +24,7 @@ func (ui *UpdateIndex) downloadIndexFile() (err error) { for _, url := range ui.IndexURLs { err = ui.downloadIndexFileFromURL(url) if err != nil { - log.Warningf("updates: %s", err) + log.Warningf("updates: failed while downloading index file %s", err) continue } // Downloading was successful. @@ -37,7 +38,7 @@ func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { client := http.Client{} resp, err := client.Get(url) if err != nil { - return fmt.Errorf("failed a get request to %s: %w", url, err) + return fmt.Errorf("failed GET request to %s: %w", url, err) } defer func() { _ = resp.Body.Close() }() filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go index 223b90de..f0e77adb 100644 --- a/service/updates/registry/registry.go +++ b/service/updates/registry/registry.go @@ -14,6 +14,12 @@ import ( var ErrNotFound error = errors.New("file not found") +const ( + defaultFileMode = os.FileMode(0o0644) + executableFileMode = os.FileMode(0o0744) + defaultDirMode = os.FileMode(0o0755) +) + type File struct { id string path string @@ -32,24 +38,19 @@ func (f *File) Version() string { } type Registry struct { - binaryUpdateIndex UpdateIndex - intelUpdateIndex UpdateIndex + updateIndex UpdateIndex - binaryBundle *Bundle - intelBundle *Bundle - - binaryUpdateBundle *Bundle - intelUpdateBundle *Bundle + bundle *Bundle + updateBundle *Bundle files map[string]File } // New create new Registry. -func New(binIndex UpdateIndex, intelIndex UpdateIndex) Registry { +func New(index UpdateIndex) Registry { return Registry{ - binaryUpdateIndex: binIndex, - intelUpdateIndex: intelIndex, - files: make(map[string]File), + updateIndex: index, + files: make(map[string]File), } } @@ -58,26 +59,20 @@ func (reg *Registry) Initialize() error { var err error // Parse current installed binary bundle. - reg.binaryBundle, err = parseBundle(reg.binaryUpdateIndex.Directory, reg.binaryUpdateIndex.IndexFile) + reg.bundle, err = parseBundle(reg.updateIndex.Directory, reg.updateIndex.IndexFile) if err != nil { return fmt.Errorf("failed to parse binary bundle: %w", err) } - // Parse current installed intel bundle. - reg.intelBundle, err = parseBundle(reg.intelUpdateIndex.Directory, reg.intelUpdateIndex.IndexFile) - if err != nil { - return fmt.Errorf("failed to parse intel bundle: %w", err) - } // Add bundle artifacts to registry. - reg.processBundle(reg.binaryBundle) - reg.processBundle(reg.intelBundle) + reg.processBundle(reg.bundle) return nil } func (reg *Registry) processBundle(bundle *Bundle) { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename) + artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath} } } @@ -89,112 +84,84 @@ func (reg *Registry) GetFile(id string) (*File, error) { return &file, nil } else { log.Errorf("updates: requested file id not found: %s", id) + for _, file := range reg.files { + log.Debugf("File: %s", file) + } return nil, ErrNotFound } } -// CheckForBinaryUpdates checks if there is a new binary bundle updates. -func (reg *Registry) CheckForBinaryUpdates() (bool, error) { - err := reg.binaryUpdateIndex.downloadIndexFile() +// CheckForUpdates checks if there is a new binary bundle updates. +func (reg *Registry) CheckForUpdates() (bool, error) { + err := reg.updateIndex.downloadIndexFile() if err != nil { return false, err } - reg.binaryUpdateBundle, err = parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + reg.updateBundle, err = parseBundle(reg.updateIndex.DownloadDirectory, reg.updateIndex.IndexFile) if err != nil { - return false, fmt.Errorf("failed to parse bundle file: %w", err) + return false, err } // TODO(vladimir): Make a better check. - if reg.binaryBundle.Version != reg.binaryUpdateBundle.Version { + if reg.bundle.Version != reg.updateBundle.Version { return true, nil } return false, nil } -// DownloadBinaryUpdates downloads available binary updates. -func (reg *Registry) DownloadBinaryUpdates() error { - if reg.binaryUpdateBundle == nil { +// DownloadUpdates downloads available binary updates. +func (reg *Registry) DownloadUpdates() error { + if reg.updateBundle == nil { // CheckForBinaryUpdates needs to be called before this. return fmt.Errorf("no valid update bundle found") } - _ = deleteUnfinishedDownloads(reg.binaryBundle.dir) - reg.binaryUpdateBundle.downloadAndVerify() + _ = deleteUnfinishedDownloads(reg.updateIndex.DownloadDirectory) + reg.updateBundle.downloadAndVerify(reg.updateIndex.DownloadDirectory) return nil } -// CheckForIntelUpdates checks if there is a new intel data bundle updates. -func (reg *Registry) CheckForIntelUpdates() (bool, error) { - err := reg.intelUpdateIndex.downloadIndexFile() +// ApplyUpdates removes the current binary folder and replaces it with the downloaded one. +func (reg *Registry) ApplyUpdates() error { + // Create purge dir. + err := os.MkdirAll(filepath.Dir(reg.updateIndex.PurgeDirectory), defaultDirMode) if err != nil { - return false, err + return fmt.Errorf("failed to create directory: %w", err) } - reg.intelUpdateBundle, err = parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) + // Read all files in the current version folder. + files, err := os.ReadDir(reg.updateIndex.Directory) if err != nil { - return false, fmt.Errorf("failed to parse bundle file: %w", err) + return err } - // TODO(vladimir): Make a better check. - if reg.intelBundle.Version != reg.intelUpdateBundle.Version { - return true, nil + // Move current version files into purge folder. + for _, file := range files { + filepath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, file.Name()) + purgePath := fmt.Sprintf("%s/%s", reg.updateIndex.PurgeDirectory, file.Name()) + err := os.Rename(filepath, purgePath) + if err != nil { + return fmt.Errorf("failed to move file %s: %w", filepath, err) + } } - return false, nil -} - -// DownloadIntelUpdates downloads available intel data updates. -func (reg *Registry) DownloadIntelUpdates() error { - if reg.intelUpdateBundle == nil { - // CheckForIntelUpdates needs to be called before this. - return fmt.Errorf("no valid update bundle found") - } - _ = deleteUnfinishedDownloads(reg.intelBundle.dir) - reg.intelUpdateBundle.downloadAndVerify() - return nil -} - -// ApplyBinaryUpdates removes the current binary folder and replaces it with the downloaded one. -func (reg *Registry) ApplyBinaryUpdates() error { - bundle, err := parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile) + // Move the new index file + indexFile := fmt.Sprintf("%s/%s", reg.updateIndex.DownloadDirectory, reg.updateIndex.IndexFile) + newIndexFile := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, reg.updateIndex.IndexFile) + err = os.Rename(indexFile, newIndexFile) if err != nil { - return fmt.Errorf("failed to parse index file: %w", err) - } - err = bundle.Verify() - if err != nil { - return fmt.Errorf("binary bundle is not valid: %w", err) + return fmt.Errorf("failed to move index file %s: %w", indexFile, err) } - err = os.RemoveAll(reg.binaryUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to remove dir: %w", err) - } - err = os.Rename(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to move dir: %w", err) - } - return nil -} - -// ApplyIntelUpdates removes the current intel folder and replaces it with the downloaded one. -func (reg *Registry) ApplyIntelUpdates() error { - bundle, err := parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile) - if err != nil { - return fmt.Errorf("failed to parse index file: %w", err) - } - err = bundle.Verify() - if err != nil { - return fmt.Errorf("binary bundle is not valid: %w", err) - } - - err = os.RemoveAll(reg.intelUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to remove dir: %w", err) - } - err = os.Rename(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.Directory) - if err != nil { - return fmt.Errorf("failed to move dir: %w", err) + // Move downloaded files to the current version folder. + for _, artifact := range reg.bundle.Artifacts { + fromFilepath := fmt.Sprintf("%s/%s", reg.updateIndex.DownloadDirectory, artifact.Filename) + toFilepath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) + err = os.Rename(fromFilepath, toFilepath) + if err != nil { + return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) + } } return nil } @@ -220,8 +187,6 @@ func parseBundle(dir string, indexFile string) (*Bundle, error) { if err != nil { return nil, err } - bundle.dir = dir - return &bundle, nil } diff --git a/service/updates/restart.go b/service/updates/restart.go index 30fc9289..66ee82d8 100644 --- a/service/updates/restart.go +++ b/service/updates/restart.go @@ -1,15 +1,12 @@ package updates import ( - "os/exec" - "runtime" "sync" "time" "github.com/tevino/abool" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/service/mgr" ) var ( @@ -86,50 +83,3 @@ func RestartNow() { restartPending.Set() // module.restartWorkerMgr.Go() } - -func automaticRestart(w *mgr.WorkerCtx) error { - // Check if the restart is still scheduled. - if restartPending.IsNotSet() { - return nil - } - - // Trigger restart. - if restartTriggered.SetToIf(false, true) { - log.Warning("updates: initiating (automatic) restart") - - // Check if we should reboot instead. - var rebooting bool - if RebootOnRestart { - // Trigger system reboot and record success. - rebooting = triggerSystemReboot() - if !rebooting { - log.Warningf("updates: rebooting failed, only restarting service instead") - } - } - - // Set restart exit code. - // if !rebooting { - // module.instance.Restart() - // } else { - // module.instance.Shutdown() - // } - } - - return nil -} - -func triggerSystemReboot() (success bool) { - switch runtime.GOOS { - case "linux": - err := exec.Command("systemctl", "reboot").Run() - if err != nil { - log.Errorf("updates: triggering reboot with systemctl failed: %s", err) - return false - } - default: - log.Warningf("updates: rebooting is not support on %s", runtime.GOOS) - return false - } - - return true -} diff --git a/spn/captain/intel.go b/spn/captain/intel.go index 6411f4c6..df04c016 100644 --- a/spn/captain/intel.go +++ b/spn/captain/intel.go @@ -22,7 +22,7 @@ var ( ) func registerIntelUpdateHook() error { - module.instance.Updates().EventResourcesUpdated.AddCallback("update SPN intel", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) { + module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("update SPN intel", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) { return false, updateSPNIntel(wc.Ctx(), nil) }) @@ -49,7 +49,7 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { } // Get intel file and load it from disk. - intelResource, err = module.instance.Updates().GetFile(intelResourcePath) + intelResource, err = module.instance.IntelUpdates().GetFile(intelResourcePath) if err != nil { return fmt.Errorf("failed to get SPN intel update: %w", err) } diff --git a/spn/captain/module.go b/spn/captain/module.go index 1f82d763..2cd16719 100644 --- a/spn/captain/module.go +++ b/spn/captain/module.go @@ -249,6 +249,6 @@ type instance interface { NetEnv() *netenv.NetEnv Patrol() *patrol.Patrol Config() *config.Config - Updates() *updates.Updates + IntelUpdates() *updates.Updates SPNGroup() *mgr.ExtendedGroup } From 8c6eb04292d46bb0dace559e85ce9697abc4f105 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 5 Sep 2024 11:21:19 +0300 Subject: [PATCH 09/62] [desktop] Fix merge issues --- desktop/tauri/src-tauri/Cargo.lock | 627 ++++++++---------- desktop/tauri/src-tauri/Cargo.toml | 2 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 1 + .../src-tauri/gen/schemas/linux-schema.json | 1 + desktop/tauri/src-tauri/src/main.rs | 39 +- desktop/tauri/src-tauri/src/traymenu.rs | 21 +- 7 files changed, 291 insertions(+), 402 deletions(-) diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 8e037303..0d3cdbaf 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -74,7 +80,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -209,58 +215,6 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "assert_matches", - "cached", - "clap 4.5.16", - "ctor", - "dark-light", - "dataurl", - "dirs 1.0.5", - "futures-util", - "gdk-pixbuf", - "gdk-pixbuf-sys", - "gio-sys", - "glib", - "glib-sys", - "gtk", - "gtk-sys", - "http", - "lazy_static", - "log", - "notify-rust", - "open", - "reqwest", - "rfd", - "rust-ini", - "serde", - "serde_json", - "sha", - "tauri", - "tauri-build", - "tauri-cli", - "tauri-plugin-clipboard-manager", - "tauri-plugin-dialog", - "tauri-plugin-log", - "tauri-plugin-notification", - "tauri-plugin-os", - "tauri-plugin-shell", - "tauri-plugin-single-instance", - "tauri-plugin-window-state", - "tauri-winrt-notification 0.3.1", - "thiserror", - "tokio", - "tokio-websockets", - "url", - "uuid", - "which", - "windows 0.54.0", - "windows-service", -] - [[package]] name = "ar" version = "0.9.0" @@ -302,7 +256,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -424,7 +378,7 @@ checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-lite 2.3.0", "slab", ] @@ -485,7 +439,7 @@ dependencies = [ "futures-lite 2.3.0", "parking", "polling 3.7.3", - "rustix 0.38.34", + "rustix 0.38.35", "slab", "tracing", "windows-sys 0.59.0", @@ -535,7 +489,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.48.0", ] @@ -554,7 +508,7 @@ dependencies = [ "cfg-if", "event-listener 5.3.1", "futures-lite 2.3.0", - "rustix 0.38.34", + "rustix 0.38.35", "tracing", "windows-sys 0.59.0", ] @@ -567,7 +521,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -582,7 +536,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix 0.38.35", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -596,13 +550,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -742,7 +696,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -843,9 +797,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.5.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" [[package]] name = "bitvec" @@ -952,10 +906,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "syn_derive", ] @@ -1061,9 +1015,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31" +checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" [[package]] name = "byteorder" @@ -1186,9 +1140,9 @@ dependencies = [ [[package]] name = "cargo-mobile2" -version = "0.15.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b8132519bea2d46174e777bd36d480d93afbe1df31c27cacfb411ff152bba1" +checksum = "93ede7b4200c8794c5fe7bc25c93a8f1756b87d50968cc20def67f2618035f65" dependencies = [ "colored", "core-foundation 0.10.0", @@ -1273,9 +1227,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.13" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "jobserver", "libc", @@ -1372,9 +1326,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.16" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -1382,9 +1336,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -1394,11 +1348,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.18" +version = "4.5.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee158892bd7ce77aa15c208abbdb73e155d191c287a659b57abd5adb92feb03" +checksum = "18d7f143a7e709cbe6c34853dcd3bb1370c7e1bb4d9e7310ca8cb40b490ae035" dependencies = [ - "clap 4.5.16", + "clap 4.5.17", ] [[package]] @@ -1410,7 +1364,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1817,7 +1771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1827,7 +1781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1873,7 +1827,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1939,7 +1893,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -1961,7 +1915,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2059,38 +2013,38 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2103,7 +2057,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2190,7 +2144,7 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.5", + "redox_users 0.4.6", "windows-sys 0.48.0", ] @@ -2201,7 +2155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users 0.4.5", + "redox_users 0.4.6", "winapi", ] @@ -2219,7 +2173,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2242,7 +2196,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2468,7 +2422,7 @@ checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" dependencies = [ "num-traits", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2489,7 +2443,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2595,7 +2549,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -2623,9 +2577,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" @@ -2673,9 +2627,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", @@ -2685,12 +2639,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -2773,7 +2727,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -2911,7 +2865,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-core", "futures-io", "parking", @@ -2926,7 +2880,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -3223,7 +3177,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -3326,14 +3280,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -3341,7 +3295,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.4.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -3546,9 +3500,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", @@ -3766,9 +3720,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3839,7 +3793,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -3895,9 +3849,9 @@ dependencies = [ [[package]] name = "iter-read" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b" +checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" [[package]] name = "itertools" @@ -4168,15 +4122,15 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0afd06142c9bcb03f4a8787c77897a87b6be9c4918f1946c33caa714c27578" +checksum = "f5f037c58cadb17e8591b620b523cc6a7ab2b91b6ce3121f8eb4171f8d80115c" dependencies = [ "ahash 0.8.11", "anyhow", "base64 0.22.1", "bytecount", - "clap 4.5.9", + "clap 4.5.17", "fancy-regex", "fraction", "getrandom 0.2.15", @@ -4370,9 +4324,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-ip-address" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" +checksum = "b435d7dd476416a905f9634dff8c330cee8d3168fdd1fbd472a17d1a75c00c3e" dependencies = [ "libc", "neli", @@ -4596,7 +4550,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -4632,6 +4586,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -4645,8 +4608,8 @@ dependencies = [ ] [[package]] -name = "muda" -version = "0.13.5" +name = "mio" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ @@ -4658,9 +4621,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c410a9d21523a819e84881603fbc00331c8001eb899964952046671deddb9c" +checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ "cocoa 0.26.0", "crossbeam-channel", @@ -4930,7 +4893,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -4980,40 +4943,19 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" -dependencies = [ - "num_enum_derive 0.7.2", + "num_enum_derive", ] [[package]] name = "num_enum_derive" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" -dependencies = [ - "proc-macro-crate 3.1.0", - "proc-macro2", - "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5166,9 +5108,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -5241,7 +5183,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5362,7 +5304,7 @@ checksum = "d0a07c44bbe07756ba25605059fa4a94543f6a75730fd8bd1105795d0b3d668d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5626,7 +5568,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5684,7 +5626,8 @@ dependencies = [ "nom", "num-bigint-dig", "num-traits", - "num_enum 0.7.2", + "num_enum", + "ocb3", "p256", "p384", "p521", @@ -5807,7 +5750,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5860,7 +5803,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -5882,7 +5825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-io", ] @@ -5920,7 +5863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.2.6", + "indexmap 2.5.0", "quick-xml 0.32.0", "serde", "time", @@ -5936,7 +5879,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -5965,7 +5908,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix 0.38.35", "tracing", "windows-sys 0.59.0", ] @@ -6001,14 +5944,14 @@ dependencies = [ "glib-sys", "gtk", "gtk-sys", - "http 1.1.0", + "http", "lazy_static", "log", "notify-rust", "open", - "reqwest 0.12.5", + "reqwest", "rfd", - "rust-ini 0.20.0", + "rust-ini", "serde", "serde_json", "sha", @@ -6042,9 +5985,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "precomputed-hash" @@ -6082,11 +6028,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit 0.22.20", ] [[package]] @@ -6144,7 +6090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -6202,9 +6148,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -6399,9 +6345,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", @@ -6463,9 +6409,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", @@ -6504,7 +6450,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -6560,9 +6506,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -6593,9 +6539,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", @@ -6611,9 +6557,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", @@ -6727,9 +6673,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.35.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec 0.7.6", "borsh", @@ -6755,9 +6701,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -6778,9 +6724,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", @@ -6835,9 +6781,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" dependencies = [ "ring", "rustls-pki-types", @@ -6924,7 +6870,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7030,9 +6976,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -7060,13 +7006,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7077,7 +7023,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7091,11 +7037,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "itoa 1.0.11", "memchr", "ryu", @@ -7120,7 +7066,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7154,7 +7100,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -7171,7 +7117,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7678,9 +7624,9 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" +checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" dependencies = [ "kurbo", "siphasher 1.0.1", @@ -7710,9 +7656,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.75" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -7728,7 +7674,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -7742,6 +7688,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "sys-locale" @@ -7767,20 +7716,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation 0.9.4", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -7801,9 +7750,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a97abbc7d6cfd0720da3e06fcb1cf2ac87cbfdb5bbbce103a1279a211c4d81" +checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e" dependencies = [ "bitflags 2.6.0", "cocoa 0.26.0", @@ -7840,13 +7789,13 @@ dependencies = [ [[package]] name = "tao-macros" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -7874,9 +7823,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.0-beta.24" +version = "2.0.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab508aad4ae86e23865e294b20a7bb89bd7afea523897b7478329b841d4295" +checksum = "0b805e6bf5f6a4df7d1a64b2952d33fca6d538746efe9c9cdae4157a1efc5b17" dependencies = [ "anyhow", "bytes", @@ -7910,7 +7859,7 @@ dependencies = [ "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "thiserror", "tokio", "tray-icon", @@ -7924,9 +7873,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.0-rc.7" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5ad5fcfaf02cf79aa6727f6c5df38567d8dce172b00b62690c6bc46c08b7ce" +checksum = "4acec578ff9de14da177722c8fb5e3d6c88af296696190c70b83bec91437248a" dependencies = [ "anyhow", "cargo_toml", @@ -7938,7 +7887,7 @@ dependencies = [ "semver", "serde", "serde_json", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "tauri-winres", "toml 0.8.19", "walkdir", @@ -7946,9 +7895,9 @@ dependencies = [ [[package]] name = "tauri-bundler" -version = "2.0.1-beta.18" +version = "2.0.1-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cc597af0bcb88c1966ccba76a6dcde5b6530c97a4fc2d798f7d3fddf182944" +checksum = "850672a9b3316d0c3abea71891d07184c6e5176b68e7c6fb0b4d5432c08f7e43" dependencies = [ "anyhow", "ar", @@ -7976,7 +7925,7 @@ dependencies = [ "tar", "tauri-icns", "tauri-macos-sign", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "tempfile", "thiserror", "time", @@ -7990,15 +7939,16 @@ dependencies = [ [[package]] name = "tauri-cli" -version = "2.0.0-beta.22" +version = "2.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24129a8dc9c3c910fca89ba0fb87ad9028868abf9dadb2514cd569fef9ad53af" +checksum = "6581f7a8e460dd8b2d9359728a90a034e7ccb0eed6d42cfb1da43f8c4842077d" dependencies = [ "anyhow", + "ar", "axum", "base64 0.22.1", "cargo-mobile2", - "clap 4.5.9", + "clap 4.5.17", "clap_complete", "colored", "common-path", @@ -8032,6 +7982,7 @@ dependencies = [ "minisign", "notify", "notify-debouncer-mini", + "object", "os_info", "os_pipe", "oxc_allocator", @@ -8052,7 +8003,7 @@ dependencies = [ "tauri-icns", "tauri-macos-sign", "tauri-utils 1.6.0", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "tempfile", "tokio", "toml 0.8.19", @@ -8065,9 +8016,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.0.0-rc.7" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809ef6316726fc72593d296cf6f4e7461326e310c313d6a6c42b6e7f1e2671cf" +checksum = "0744bec087358e5de9a078a1b19346ed9b775f578395975f5a74ccd0c717b22a" dependencies = [ "base64 0.22.1", "brotli", @@ -8081,8 +8032,8 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.75", - "tauri-utils 2.0.0-rc.7", + "syn 2.0.77", + "tauri-utils 2.0.0-rc.8", "thiserror", "time", "url", @@ -8121,23 +8072,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-rc.6" +version = "2.0.0-rc.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1359e8861d210d25731f8b1bfbb4d111dd06406cf73c59659366ef450364d811" +checksum = "b043cac341130f288044dca76fae8e62d7c18fdcd8012239a66af03868b7ca37" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "tauri-codegen", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", ] [[package]] name = "tauri-plugin" -version = "2.0.0-rc.3" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec01af01098a286d3e430c1fa947bfd77bc8011ecb209438af4444b02d82b29e" +checksum = "1c9bb31aad7296f85df545171023c72a263b54aac350197f923893fb5e6f90b4" dependencies = [ "anyhow", "glob", @@ -8145,16 +8096,16 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils 2.0.0-beta.19", - "toml 0.8.15", + "tauri-utils 2.0.0-rc.8", + "toml 0.8.19", "walkdir", ] [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.0.0-rc.1" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6719e5f3fcc6c6f5afda68da944a44a50c28f30a3506c457ea7dbcc13377bfe0" +checksum = "4a9d78deb78704d9eeedc23892fc6b25a944c7f9c59354a0ea9a4fb5ab043794" dependencies = [ "arboard", "image 0.24.9", @@ -8168,9 +8119,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.0.0-beta.11" +version = "2.0.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8860dd73c96969eb14813f9f04d8665f2853342670456fb6619d637137ef0d09" +checksum = "191a81b01083881a388879e269eec60b582eed6fc3e16884dc394ff1791b0c19" dependencies = [ "dunce", "log", @@ -8182,13 +8133,14 @@ dependencies = [ "tauri-plugin", "tauri-plugin-fs", "thiserror", + "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.0.0-beta.11" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461853268fe115ca19ee21e5986d505944f0b826048fe1bd726d74753fdf1df6" +checksum = "0704205734f3b4f37d7bf9e9072537716371741bf98b22a22cc5e5340e512467" dependencies = [ "anyhow", "glob", @@ -8205,9 +8157,9 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.0.0-beta.9" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f80d78a6e8102acf05a1e735f006991a2abfc71566d4e484f820b7495cd52c" +checksum = "b57e4666c4a5d81f81b7bb8eacf51ae32c4e69c35071aabb480ad20a80836e4e" dependencies = [ "android_logger", "byte-unit", @@ -8227,9 +8179,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.0.0-beta.10" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a09eb4d9e0919ce954da68d3707ddb161e5e37447da26609d0d0f5aebbc69a" +checksum = "6bf32fba3a2650f89c7dfc532b38de566a37779e482f07dccabf45fe24444e26" dependencies = [ "log", "notify-rust", @@ -8246,9 +8198,9 @@ dependencies = [ [[package]] name = "tauri-plugin-os" -version = "2.0.0-beta.8" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a0466f11f45fd3f640a17b5ba5e34c62912c9b391141c818155125ae9f0917" +checksum = "ebc4ee761edd532fce2232453e9c8e0f7d9c0b6fe125c4b90b3eb4362ee84224" dependencies = [ "gethostname", "log", @@ -8264,9 +8216,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.0-beta.9" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9fa8c4e3d9ec343f6c3eb081672045566128a6c48ff6f6eeea85251ff38d3f" +checksum = "e83800ddf78b820172efb5ed7310344e8e4f97fd30cd8237a3f20c12a79eb136" dependencies = [ "encoding_rs", "log", @@ -8285,24 +8237,24 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.0.0-beta.11" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b21866e185e2f9c5d40afb851441e3292a4f94f4a26af6ae0dff6e7e5ba03f42" +checksum = "d73c92c98d44d4daba0118d905f45243dfcd6eaac82216c3382a02d17cb74cf2" dependencies = [ "log", "serde", "serde_json", "tauri", "thiserror", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "zbus 4.4.0", ] [[package]] name = "tauri-plugin-window-state" -version = "2.0.0-beta.11" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f7cea222b8eeb3598c7b3e19e9c7e6b1c2d60207b87225e0c3bb1c24c8fdec" +checksum = "303569dd7858361d4c623845448b136b4c95d53b5d2bde6630bea9d7f0022d45" dependencies = [ "bitflags 2.6.0", "log", @@ -8315,9 +8267,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-beta.20" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe978df03966febbebc608931dc2cf26ef94df70855a18b05f07134cf474de09" +checksum = "d7c7a6530acc06640e8f07cfeb01ac694f1de2f4e565525a2199e0dca80ff9f7" dependencies = [ "dpi", "gtk", @@ -8326,7 +8278,7 @@ dependencies = [ "raw-window-handle", "serde", "serde_json", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "thiserror", "url", "windows 0.58.0", @@ -8334,9 +8286,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-beta.20" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11e4d568f61095f507b3fc4254dfbfff3b20de2a1d66167ffca3f6d90b14db8f" +checksum = "5fcadbc24646c8d3362ed4e332cb42932e08c632220a20a61cb7e5fe36ddd85c" dependencies = [ "cocoa 0.26.0", "gtk", @@ -8348,7 +8300,7 @@ dependencies = [ "softbuffer", "tao", "tauri-runtime", - "tauri-utils 2.0.0-rc.7", + "tauri-utils 2.0.0-rc.8", "url", "webkit2gtk", "webview2-com", @@ -8389,9 +8341,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.0-rc.7" +version = "2.0.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53d9fe87e985b273696ae22ce2b9f099a8f1b44bc8fb127467bda5fcb3e4371" +checksum = "201498c8281ab2597e344b4a4c923e8d491782305979d71e7cdf8fb79aab5948" dependencies = [ "aes-gcm", "brotli", @@ -8465,9 +8417,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", - "fastrand 2.1.0", + "fastrand 2.1.1", "once_cell", - "rustix 0.38.34", + "rustix 0.38.35", "windows-sys 0.59.0", ] @@ -8536,7 +8488,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -8645,15 +8597,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", @@ -8670,7 +8621,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -8731,9 +8682,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -8781,7 +8732,7 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8803,7 +8754,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8816,18 +8767,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.6", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "toml_datetime", "winnow 0.5.40", ] @@ -8838,7 +8778,7 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -8893,7 +8833,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9125,9 +9065,9 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-script" @@ -9428,7 +9368,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -9462,7 +9402,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9542,9 +9482,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ "rustls-pki-types", ] @@ -9571,7 +9511,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9593,13 +9533,13 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "6.0.1" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "rustix 0.38.34", + "rustix 0.38.35", "winsafe", ] @@ -9744,7 +9684,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9755,7 +9695,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9766,7 +9706,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -9777,7 +9717,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -10089,9 +10029,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wry" -version = "0.42.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b8049c8f239cdbfaaea4bacb9646f6b208938ceec0acd5b3e99cd05f70903f" +checksum = "f4d715cf5fe88e9647f3d17b207b6d060d4a88e7171d4ccb2d2c657dd1d44728" dependencies = [ "base64 0.22.1", "block", @@ -10164,7 +10104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix 0.38.34", + "rustix 0.38.35", "x11rb-protocol", ] @@ -10213,7 +10153,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.14", - "rustix 0.38.34", + "rustix 0.38.35", ] [[package]] @@ -10347,10 +10287,10 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "zvariant_utils 2.1.0", ] @@ -10376,22 +10316,13 @@ dependencies = [ "zvariant 4.2.0", ] -[[package]] -name = "zerocopy" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", -] - [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -10403,7 +10334,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] @@ -10423,21 +10354,21 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] [[package]] name = "zip" -version = "2.1.6" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.4.0", + "indexmap 2.5.0", "memchr", "thiserror", "zopfli", @@ -10557,10 +10488,10 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ - "proc-macro-crate 3.1.0", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", "zvariant_utils 2.1.0", ] @@ -10583,5 +10514,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.77", ] diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index f89ca02f..f38c9679 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -27,7 +27,7 @@ tauri-plugin-log = "2.0.0-rc" tauri-plugin-window-state = "2.0.0-rc" tauri-cli = "2.0.0-rc.8" -clap = { version = "4" } +clap_lex = "0.7.2" # General serde_json = "1.0" diff --git a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json index 85397e54..233ccc01 100644 --- a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json +++ b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"clipboard-manager":{"default_permission":null,"permissions":{"allow-read":{"identifier":"allow-read","description":"Enables the read command without any pre-configured scope.","commands":{"allow":["read"],"deny":[]}},"allow-write":{"identifier":"allow-write","description":"Enables the write command without any pre-configured scope.","commands":{"allow":["write"],"deny":[]}},"deny-read":{"identifier":"deny-read","description":"Denies the read command without any pre-configured scope.","commands":{"allow":[],"deny":["read"]}},"deny-write":{"identifier":"deny-write","description":"Denies the write command without any pre-configured scope.","commands":{"allow":[],"deny":["write"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} +{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json index 2119bf21..797ccb5c 100644 --- a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json @@ -3072,3 +3072,4 @@ ] } } +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json index 2119bf21..797ccb5c 100644 --- a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json @@ -3072,3 +3072,4 @@ ] } } +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index c0adcb5c..7d605021 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -13,8 +13,8 @@ mod service; mod xdg; // App modules -mod config; mod cli; +mod config; mod portmaster; mod traymenu; mod window; @@ -126,32 +126,8 @@ fn main() { let cli_args = cli::parse(std::env::args()); - let mut cli = CliArguments { - data: None, - log: LOG_LEVEL.to_string(), - background: false, - with_prompts: false, - with_notifications: false, - }; - - if let Some(data) = matches.get_one::("data") { - cli.data = Some(data.to_string()); - } - - if let Some(log) = matches.get_one::("log") { - cli.log = log.to_string(); - } - - if let Some(value) = matches.get_one::("with_prompts") { - cli.with_prompts = *value; - } - - if let Some(value) = matches.get_one::("with_notifications") { - cli.with_notifications = *value; - } - #[cfg(target_os = "linux")] - let log_target = if let Some(data_dir) = cli.data { + let log_target = if let Some(data_dir) = cli_args.data { tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder { path: Path::new(&format!("{}/logs/app2", data_dir)).into(), file_name: None, @@ -168,17 +144,6 @@ fn main() { tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout) }; - let mut log_level = LOG_LEVEL; - match cli.log.as_ref() { - "off" => log_level = LevelFilter::Off, - "error" => log_level = LevelFilter::Error, - "warn" => log_level = LevelFilter::Warn, - "info" => log_level = LevelFilter::Info, - "debug" => log_level = LevelFilter::Debug, - "trace" => log_level = LevelFilter::Trace, - _ => {} - } - let app = tauri::Builder::default() // Shell plugin for open_external support .plugin(tauri_plugin_shell::init()) diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 600af46d..127c2194 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -1,18 +1,18 @@ use std::ops::Deref; use std::sync::atomic::AtomicBool; -use std::sync::{Mutex, RwLock}; +use std::sync::RwLock; use std::{collections::HashMap, sync::atomic::Ordering}; use log::{debug, error}; use tauri::menu::{Menu, MenuItemKind}; use tauri::tray::{MouseButton, MouseButtonState}; -use tauri::Runtime; use tauri::{ image::Image, menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, tray::{TrayIcon, TrayIconBuilder}, Wry, }; +use tauri::{Manager, Runtime}; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use crate::config; @@ -91,13 +91,6 @@ fn get_red_icon() -> &'static [u8] { const DARK_RED_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); match get_theme_mode() { - const DARK_RED_ICON: &'static [u8] = - include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); - let mode = dark_light::detect(); - match mode { - const DARK_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); - let mode = dark_light::detect(); - match mode { dark_light::Mode::Light => DARK_RED_ICON, _ => LIGHT_RED_ICON, } @@ -142,10 +135,7 @@ pub fn setup_tray_menu( .enabled(false) .build(app) .unwrap(); - { - let mut button_ref = SPN_STATUS.lock()?; - *button_ref = Some(spn_status.clone()); - } + // Setup SPN button let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, "Enable SPN") .build(app) @@ -496,7 +486,9 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { } } } - update_spn_ui_state(false); + if let Some(menu) = app.menu() { + update_spn_ui_state(menu, false); + } update_icon_color(&icon, IconColor::Red); } @@ -564,7 +556,6 @@ fn save_theme(app: &tauri::AppHandle, mode: dark_light::Mode) { if let Some(menu) = app.menu() { update_spn_ui_state(menu, false); } - _ = icon.set_icon(Some(Image::from_bytes(get_red_icon()).unwrap())); } fn update_spn_ui_state(menu: Menu, enabled: bool) { From 83ec18f552cca4a7ef62f381f0f5f631243e740b Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 11 Sep 2024 18:52:36 +0300 Subject: [PATCH 10/62] [WIP] Updater support for windows --- cmds/portmaster-core/main.go | 94 +-------- cmds/portmaster-core/main_linux.go | 100 ++++++++++ cmds/portmaster-core/main_windows.go | 180 ++++++++++++++++++ desktop/tauri/src-tauri/src/main.rs | 1 + desktop/tauri/src-tauri/tauri.conf.json5 | 23 ++- .../interception/interception_windows.go | 7 +- service/firewall/interception/module.go | 5 +- .../interception/windowskext2/kext.go | 3 + service/instance.go | 60 ++++-- service/updates/registry/bundle.go | 9 + service/updates/registry/registry.go | 4 +- 11 files changed, 371 insertions(+), 115 deletions(-) create mode 100644 cmds/portmaster-core/main_linux.go create mode 100644 cmds/portmaster-core/main_windows.go diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index bc9f4f28..630f5827 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -8,14 +8,11 @@ import ( "io" "log/slog" "os" - "os/signal" "runtime" "runtime/pprof" "syscall" - "time" "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service" "github.com/safing/portmaster/service/mgr" @@ -36,6 +33,11 @@ func init() { } func main() { + instance := initialize() + run(instance) +} + +func initialize() *service.Instance { flag.Parse() // set information @@ -80,91 +82,7 @@ func main() { } os.Exit(0) } - - // Set default log level. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() - - // Start - go func() { - err = instance.Start() - if err != nil { - fmt.Printf("instance start failed: %s\n", err) - - // Print stack on start failure, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - } - - os.Exit(1) - } - }() - - // Wait for signal. - signalCh := make(chan os.Signal, 1) - if enableInputSignals { - go inputSignals(signalCh) - } - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - sigUSR1, - ) - - select { - case sig := <-signalCh: - // Only print and continue to wait if SIGUSR1 - if sig == sigUSR1 { - printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") - } else { - fmt.Println(" ") // CLI output. - slog.Warn("program was interrupted, stopping") - } - - case <-instance.Stopped(): - log.Shutdown() - os.Exit(instance.ExitCode()) - } - - // Catch signals during shutdown. - // Rapid unplanned disassembly after 5 interrupts. - go func() { - forceCnt := 5 - for { - <-signalCh - forceCnt-- - if forceCnt > 0 { - fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) - } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") - os.Exit(1) - } - } - }() - - // Rapid unplanned disassembly after 3 minutes. - go func() { - time.Sleep(3 * time.Minute) - printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") - os.Exit(1) - }() - - // Stop instance. - if err := instance.Stop(); err != nil { - slog.Error("failed to stop", "err", err) - } - log.Shutdown() - - // Print stack on shutdown, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON EXIT") - } - - os.Exit(instance.ExitCode()) + return instance } func printStackTo(writer io.Writer, msg string) { diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go new file mode 100644 index 00000000..aa8d9a60 --- /dev/null +++ b/cmds/portmaster-core/main_linux.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" +) + +func run(instance *service.Instance) { + // Set default log level. + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + + // Start + go func() { + err := instance.Start() + if err != nil { + fmt.Printf("instance start failed: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + } + + os.Exit(1) + } + }() + + // Wait for signal. + signalCh := make(chan os.Signal, 1) + if enableInputSignals { + go inputSignals(signalCh) + } + signal.Notify( + signalCh, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + sigUSR1, + ) + + select { + case sig := <-signalCh: + // Only print and continue to wait if SIGUSR1 + if sig == sigUSR1 { + printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") + } else { + fmt.Println(" ") // CLI output. + slog.Warn("program was interrupted, stopping") + } + + case <-instance.Stopped(): + log.Shutdown() + os.Exit(instance.ExitCode()) + } + + // Catch signals during shutdown. + // Rapid unplanned disassembly after 5 interrupts. + go func() { + forceCnt := 5 + for { + <-signalCh + forceCnt-- + if forceCnt > 0 { + fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) + } else { + printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + os.Exit(1) + } + } + }() + + // Rapid unplanned disassembly after 3 minutes. + go func() { + time.Sleep(3 * time.Minute) + printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") + os.Exit(1) + }() + + // Stop instance. + if err := instance.Stop(); err != nil { + slog.Error("failed to stop", "err", err) + } + log.Shutdown() + + // Print stack on shutdown, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON EXIT") + } + + os.Exit(instance.ExitCode()) +} diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go new file mode 100644 index 00000000..61d4f840 --- /dev/null +++ b/cmds/portmaster-core/main_windows.go @@ -0,0 +1,180 @@ +package main + +// Based on the official Go examples from +// https://github.com/golang/sys/blob/master/windows/svc/example +// by The Go Authors. +// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" +) + +var ( + // wait groups + runWg sync.WaitGroup + finishWg sync.WaitGroup +) + +const serviceName = "PortmasterCore" + +type windowsService struct { + instance *service.Instance +} + +func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + + startupComplete := make(chan struct{}) + go func() { + for !ws.instance.Ready() { + time.Sleep(1 * time.Second) + } + startupComplete <- struct{}{} + }() + +service: + for { + select { + case <-startupComplete: + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + case <-ws.instance.Stopped(): + changes <- svc.Status{State: svc.StopPending} + break service + case c := <-changeRequests: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + ws.instance.Shutdown() + default: + log.Errorf("unexpected control request: #%d\n", c) + } + } + } + + // wait until everything else is finished + finishWg.Wait() + + log.Shutdown() + + // send stopped status + changes <- svc.Status{State: svc.Stopped} + // wait a little for the status to reach Windows + time.Sleep(100 * time.Millisecond) + + return ssec, errno +} + +func run(instance *service.Instance) error { + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + // check if we are running interactively + isService, err := svc.IsWindowsService() + if err != nil { + return fmt.Errorf("could not determine if running interactively: %s", err) + } + // select service run type + svcRun := svc.Run + if !isService { + log.Warningf("running interactively, switching to debug execution (no real service).\n") + svcRun = debug.Run + go registerSignalHandler(instance) + } + + runWg.Add(2) + + // run service client + go func() { + sErr := svcRun(serviceName, &windowsService{ + instance: instance, + }) + if sErr != nil { + log.Infof("shuting down service with error: %s", sErr) + } else { + log.Infof("shuting down service") + } + instance.Shutdown() + runWg.Done() + }() + + finishWg.Add(1) + // run service + go func() { + // run slightly delayed + time.Sleep(250 * time.Millisecond) + instance.Start() + + if err != nil { + fmt.Printf("instance start failed: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + } + + } + runWg.Done() + finishWg.Done() + }() + + runWg.Wait() + + return err +} + +func registerSignalHandler(instance *service.Instance) { + // Wait for signal. + signalCh := make(chan os.Signal, 1) + if enableInputSignals { + go inputSignals(signalCh) + } + signal.Notify( + signalCh, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + sigUSR1, + ) + + select { + case sig := <-signalCh: + // Only print and continue to wait if SIGUSR1 + if sig == sigUSR1 { + printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") + } else { + fmt.Println(" ") // CLI output. + slog.Warn("program was interrupted, stopping") + instance.Shutdown() + } + } + + // Catch signals during shutdown. + // Rapid unplanned disassembly after 5 interrupts. + go func() { + forceCnt := 5 + for { + <-signalCh + forceCnt-- + if forceCnt > 0 { + fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) + } else { + printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + os.Exit(1) + } + } + }() +} diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 7d605021..7dd9df42 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -120,6 +120,7 @@ fn show_webview_not_installed_dialog() -> i32 { } fn main() { + env::set_var("GDK_BACKEND", "x11"); if tauri::webview_version().is_err() { std::process::exit(show_webview_not_installed_dialog()); } diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index beba5cd7..a2d11d8a 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -60,6 +60,17 @@ "desktopTemplate": "../../../packaging/linux/portmaster.desktop", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", + "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", + "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -73,7 +84,17 @@ "release": "1", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", + "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -106,4 +127,4 @@ "../../../assets/data/icons/pm_light.ico" ] } -} \ No newline at end of file +} diff --git a/service/firewall/interception/interception_windows.go b/service/firewall/interception/interception_windows.go index cb97376b..3f9df6f8 100644 --- a/service/firewall/interception/interception_windows.go +++ b/service/firewall/interception/interception_windows.go @@ -10,14 +10,13 @@ import ( "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network" "github.com/safing/portmaster/service/network/packet" - "github.com/safing/portmaster/service/updates" ) var useOldKext = false // start starts the interception. func startInterception(packets chan packet.Packet) error { - kextFile, err := updates.GetPlatformFile("kext/portmaster-kext.sys") + kextFile, err := module.instance.BinaryUpdates().GetFile("portmaster-kext.sys") if err != nil { return fmt.Errorf("interception: could not get kext sys: %s", err) } @@ -77,7 +76,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) @@ -95,7 +93,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) @@ -112,7 +109,6 @@ func startInterception(packets chan packet.Packet) error { case <-w.Done(): return nil } - } }) } @@ -159,5 +155,4 @@ func GetKextVersion() (string, error) { } return version.String(), nil } - } diff --git a/service/firewall/interception/module.go b/service/firewall/interception/module.go index 072eb3b5..158432e0 100644 --- a/service/firewall/interception/module.go +++ b/service/firewall/interception/module.go @@ -8,6 +8,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/updates" ) // Interception is the packet interception module. @@ -97,4 +98,6 @@ func New(instance instance) (*Interception, error) { return module, nil } -type instance interface{} +type instance interface { + BinaryUpdates() *updates.Updates +} diff --git a/service/firewall/interception/windowskext2/kext.go b/service/firewall/interception/windowskext2/kext.go index ed15476e..43be43cd 100644 --- a/service/firewall/interception/windowskext2/kext.go +++ b/service/firewall/interception/windowskext2/kext.go @@ -62,6 +62,9 @@ func GetKextServiceHandle() windows.Handle { // Stop intercepting. func Stop() error { + if kextFile == nil { + return fmt.Errorf("kextfile is nil") + } // Prepare kernel for shutdown err := shutdownRequest() if err != nil { diff --git a/service/instance.go b/service/instance.go index 43c7562b..8f63a78b 100644 --- a/service/instance.go +++ b/service/instance.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + go_runtime "runtime" "sync/atomic" "time" @@ -47,23 +48,6 @@ import ( "github.com/safing/portmaster/spn/terminal" ) -var binaryUpdateIndex = registry.UpdateIndex{ - Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/lib/portmaster/new_bin", - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, -} - -var intelUpdateIndex = registry.UpdateIndex{ - Directory: "/var/lib/portmaster/intel", - DownloadDirectory: "/var/lib/portmaster/new_intel", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, -} - // Instance is an instance of a Portmaster service. type Instance struct { ctx context.Context @@ -121,6 +105,48 @@ type Instance struct { // New returns a new Portmaster service instance. func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx + var binaryUpdateIndex registry.UpdateIndex + var intelUpdateIndex registry.UpdateIndex + if go_runtime.GOOS == "windows" { + binaryUpdateIndex = registry.UpdateIndex{ + Directory: "C:/Program Files/Portmaster/binary", + DownloadDirectory: "C:/Program Files/Portmaster/new_binary", + PurgeDirectory: "C:/Program Files/Portmaster/old_binary", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + intelUpdateIndex = registry.UpdateIndex{ + Directory: "C:/Program Files/Portmaster/intel", + DownloadDirectory: "C:/Program Files/Portmaster/new_intel", + PurgeDirectory: "C:/Program Files/Portmaster/old_intel", + IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, + } + } else if go_runtime.GOOS == "linux" { + binaryUpdateIndex = registry.UpdateIndex{ + Directory: "/usr/lib/portmaster", + DownloadDirectory: "/var/lib/portmaster/new_bin", + PurgeDirectory: "/var/lib/portmaster/old_bin", + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: []string{"http://localhost:8000/test-binary.json"}, + IndexFile: "bin-index.json", + AutoApply: false, + } + + intelUpdateIndex = registry.UpdateIndex{ + Directory: "/var/lib/portmaster/intel", + DownloadDirectory: "/var/lib/portmaster/new_intel", + PurgeDirectory: "/var/lib/portmaster/intel_bin", + IndexURLs: []string{"http://localhost:8000/test-intel.json"}, + IndexFile: "intel-index.json", + AutoApply: true, + } + } + // Create instance to pass it to modules. instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) diff --git a/service/updates/registry/bundle.go b/service/updates/registry/bundle.go index 24953966..4df887ad 100644 --- a/service/updates/registry/bundle.go +++ b/service/updates/registry/bundle.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "time" "github.com/safing/portmaster/base/log" @@ -19,6 +20,8 @@ import ( const MaxUnpackSize = 1 << 30 // 2^30 == 1GB +const current_platform = runtime.GOOS + "_" + runtime.GOARCH + type Artifact struct { Filename string `json:"Filename"` SHA256 string `json:"SHA256"` @@ -107,6 +110,11 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } func processArtifact(client *http.Client, artifact Artifact, filePath string) error { + // Skip artifacts not meant for this machine. + if artifact.Platform != "" && artifact.Platform != current_platform { + return nil + } + providedHash, err := hex.DecodeString(artifact.SHA256) if err != nil || len(providedHash) != sha256.Size { return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) @@ -149,6 +157,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er if err != nil { return fmt.Errorf("failed to write to file: %w", err) } + file.Close() // Rename err = os.Rename(tmpFilename, filePath) diff --git a/service/updates/registry/registry.go b/service/updates/registry/registry.go index f0e77adb..f670df26 100644 --- a/service/updates/registry/registry.go +++ b/service/updates/registry/registry.go @@ -125,7 +125,7 @@ func (reg *Registry) DownloadUpdates() error { // ApplyUpdates removes the current binary folder and replaces it with the downloaded one. func (reg *Registry) ApplyUpdates() error { // Create purge dir. - err := os.MkdirAll(filepath.Dir(reg.updateIndex.PurgeDirectory), defaultDirMode) + err := os.MkdirAll(reg.updateIndex.PurgeDirectory, defaultDirMode) if err != nil { return fmt.Errorf("failed to create directory: %w", err) } @@ -198,7 +198,7 @@ func deleteUnfinishedDownloads(rootDir string) error { // Check if the current file has the specified extension if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { - log.Warningf("updates deleting unfinished: %s\n", path) + log.Warningf("updates: deleting unfinished: %s\n", path) err := os.Remove(path) if err != nil { return fmt.Errorf("failed to delete file %s: %w", path, err) From 84a15b5fb30ee6bead52ee46848e742bb8cce4fc Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 17 Sep 2024 12:36:44 +0300 Subject: [PATCH 11/62] [WIP] updater rafactoring, minor improvments --- cmds/portmaster-core/main_windows.go | 50 ++--- desktop/tauri/src-tauri/README.md | 2 + desktop/tauri/src-tauri/tauri.conf.json5 | 4 +- desktop/tauri/src-tauri/templates/files.wxs | 36 +++ desktop/tauri/src-tauri/templates/main.wxs | 10 +- desktop/tauri/src-tauri/templates/service.wxs | 2 +- service/instance.go | 50 +++-- service/intel/filterlists/database.go | 10 +- service/intel/filterlists/index.go | 4 +- service/intel/filterlists/updater.go | 10 +- service/intel/geoip/database.go | 6 +- service/ui/serve.go | 4 +- service/updates/{registry => }/bundle.go | 75 ++++++- service/updates/{registry => }/index.go | 4 +- service/updates/module.go | 131 ++++++++--- service/updates/registry/registry.go | 210 ------------------ service/updates/updater.go | 78 +++++++ spn/captain/intel.go | 4 +- 18 files changed, 380 insertions(+), 310 deletions(-) create mode 100644 desktop/tauri/src-tauri/templates/files.wxs rename service/updates/{registry => }/bundle.go (75%) rename service/updates/{registry => }/index.go (93%) delete mode 100644 service/updates/registry/registry.go create mode 100644 service/updates/updater.go diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go index 61d4f840..9b3798c3 100644 --- a/cmds/portmaster-core/main_windows.go +++ b/cmds/portmaster-core/main_windows.go @@ -35,20 +35,12 @@ type windowsService struct { func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown changes <- svc.Status{State: svc.StartPending} - - startupComplete := make(chan struct{}) - go func() { - for !ws.instance.Ready() { - time.Sleep(1 * time.Second) - } - startupComplete <- struct{}{} - }() + ws.instance.Start() + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} service: for { select { - case <-startupComplete: - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} case <-ws.instance.Stopped(): changes <- svc.Status{State: svc.StopPending} break service @@ -59,13 +51,13 @@ service: case svc.Stop, svc.Shutdown: ws.instance.Shutdown() default: - log.Errorf("unexpected control request: #%d\n", c) + log.Errorf("unexpected control request: #%d", c) } } } // wait until everything else is finished - finishWg.Wait() + // finishWg.Wait() log.Shutdown() @@ -88,12 +80,12 @@ func run(instance *service.Instance) error { // select service run type svcRun := svc.Run if !isService { - log.Warningf("running interactively, switching to debug execution (no real service).\n") + log.Warningf("running interactively, switching to debug execution (no real service).") svcRun = debug.Run go registerSignalHandler(instance) } - runWg.Add(2) + runWg.Add(1) // run service client go func() { @@ -105,29 +97,27 @@ func run(instance *service.Instance) error { } else { log.Infof("shuting down service") } - instance.Shutdown() runWg.Done() }() - finishWg.Add(1) + // finishWg.Add(1) // run service - go func() { - // run slightly delayed - time.Sleep(250 * time.Millisecond) - instance.Start() + // go func() { + // // run slightly delayed + // time.Sleep(250 * time.Millisecond) - if err != nil { - fmt.Printf("instance start failed: %s\n", err) + // if err != nil { + // fmt.Printf("instance start failed: %s\n", err) - // Print stack on start failure, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - } + // // Print stack on start failure, if enabled. + // if printStackOnExit { + // printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") + // } - } - runWg.Done() - finishWg.Done() - }() + // } + // runWg.Done() + // finishWg.Done() + // }() runWg.Wait() diff --git a/desktop/tauri/src-tauri/README.md b/desktop/tauri/src-tauri/README.md index ad238810..a057e285 100644 --- a/desktop/tauri/src-tauri/README.md +++ b/desktop/tauri/src-tauri/README.md @@ -33,7 +33,9 @@ Update WIX installer template: 2. Replace the contents of `templates/main_original.wxs` with the repository version. 3. Replace the contents of `templates/main.wsx` and add the fallowing lines at the end of the file, inside the `Product` tag. ```xml + + ``` diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index a2d11d8a..2d9de0d7 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -109,8 +109,10 @@ }, "wix": { "fragmentPaths": [ - "templates/service.wxs" + "templates/service.wxs", + "templates/files.wxs" ], + "componentGroupRefs": ["BinaryAndIntelFiles"], "template": "templates/main.wxs" } }, diff --git a/desktop/tauri/src-tauri/templates/files.wxs b/desktop/tauri/src-tauri/templates/files.wxs new file mode 100644 index 00000000..0eec08f1 --- /dev/null +++ b/desktop/tauri/src-tauri/templates/files.wxs @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/tauri/src-tauri/templates/main.wxs b/desktop/tauri/src-tauri/templates/main.wxs index 249eccf0..f42ba777 100644 --- a/desktop/tauri/src-tauri/templates/main.wxs +++ b/desktop/tauri/src-tauri/templates/main.wxs @@ -341,14 +341,16 @@ {{/if}} - - - - AUTOLAUNCHAPP AND NOT Installed + + + + + + \ No newline at end of file diff --git a/desktop/tauri/src-tauri/templates/service.wxs b/desktop/tauri/src-tauri/templates/service.wxs index 31ff064c..52f5339e 100644 --- a/desktop/tauri/src-tauri/templates/service.wxs +++ b/desktop/tauri/src-tauri/templates/service.wxs @@ -3,7 +3,7 @@ Date: Wed, 18 Sep 2024 18:26:34 +0300 Subject: [PATCH 12/62] [WIP] Refactoring and bug fixes --- service/core/api.go | 26 +++++++ service/core/core.go | 3 + service/updates/bundle.go | 94 ++++++++++------------- service/updates/index.go | 2 +- service/updates/module.go | 148 ++++++++++++++++++++++++++++--------- service/updates/updater.go | 24 +++--- 6 files changed, 196 insertions(+), 101 deletions(-) diff --git a/service/core/api.go b/service/core/api.go index c633956e..aa1305e0 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -106,6 +106,32 @@ func registerAPIEndpoints() error { return err } + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "updates/check", + Read: api.PermitUser, + ActionFunc: func(ar *api.Request) (string, error) { + module.instance.BinaryUpdates().TriggerUpdateCheck() + module.instance.IntelUpdates().TriggerUpdateCheck() + return "update check triggered", nil + }, + Name: "Get the ID of the calling profile", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "updates/apply", + Read: api.PermitUser, + ActionFunc: func(ar *api.Request) (string, error) { + module.instance.BinaryUpdates().TriggerApplyUpdates() + module.instance.IntelUpdates().TriggerApplyUpdates() + return "upgrade triggered", nil + }, + Name: "Get the ID of the calling profile", + }); err != nil { + return err + } + return nil } diff --git a/service/core/core.go b/service/core/core.go index ecbcf948..60ad5857 100644 --- a/service/core/core.go +++ b/service/core/core.go @@ -16,6 +16,7 @@ import ( _ "github.com/safing/portmaster/service/status" _ "github.com/safing/portmaster/service/sync" _ "github.com/safing/portmaster/service/ui" + "github.com/safing/portmaster/service/updates" ) // Core is the core service module. @@ -114,4 +115,6 @@ func New(instance instance) (*Core, error) { type instance interface { Shutdown() AddWorkerInfoToDebugInfo(di *debug.Info) + BinaryUpdates() *updates.Updates + IntelUpdates() *updates.Updates } diff --git a/service/updates/bundle.go b/service/updates/bundle.go index af0c1021..c2de5a92 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -58,44 +58,54 @@ func ParseBundle(dir string, indexFile string) (*Bundle, error) { var bundle Bundle err = json.Unmarshal(content, &bundle) if err != nil { - return nil, err + return nil, fmt.Errorf("%s %w", filepath, err) } + + // Filter artifacts + filtered := make([]Artifact, 0) + for _, a := range bundle.Artifacts { + if a.Platform == "" || a.Platform == currentPlatform { + filtered = append(filtered, a) + } + } + bundle.Artifacts = filtered + return &bundle, nil } // CopyMatchingFilesFromCurrent check if there the current bundle files has matching files with the new bundle and copies them if they match. func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, newDir string) error { + // Make sure new dir exists + _ = os.MkdirAll(newDir, defaultDirMode) + for _, currentArtifact := range current.Artifacts { new: for _, newArtifact := range bundle.Artifacts { if currentArtifact.Filename == newArtifact.Filename { if currentArtifact.SHA256 == newArtifact.SHA256 { - // Files match, make sure new dir exists - _ = os.MkdirAll(newDir, defaultDirMode) - - // Open the current file - sourceFilePath := fmt.Sprintf("%s/%s", currentDir, newArtifact.Filename) - sourceFile, err := os.Open(sourceFilePath) + // Read the content of the current file. + sourceFilePath := filepath.Join(currentDir, newArtifact.Filename) + content, err := os.ReadFile(sourceFilePath) if err != nil { - return fmt.Errorf("failed to open %s file: %w", sourceFilePath, err) + return fmt.Errorf("failed to read file %s: %w", sourceFilePath, err) + } + + // Check if the content matches the artifact hash + expectedHash, err := hex.DecodeString(newArtifact.SHA256) + if err != nil || len(expectedHash) != sha256.Size { + return fmt.Errorf("invalid artifact hash %s: %w", newArtifact.SHA256, err) + } + hash := sha256.Sum256(content) + if !bytes.Equal(expectedHash, hash[:]) { + return fmt.Errorf("expected and file hash mismatch: %s", sourceFilePath) } - defer sourceFile.Close() // Create new file - destFilePath := fmt.Sprintf("%s/%s", newDir, newArtifact.Filename) - destFile, err := os.Create(destFilePath) + destFilePath := filepath.Join(newDir, newArtifact.Filename) + err = os.WriteFile(destFilePath, content, defaultFileMode) if err != nil { - return fmt.Errorf("failed to create %s file: %w", destFilePath, err) + return fmt.Errorf("failed to write to file %s: %w", destFilePath, err) } - defer destFile.Close() - - // Copy - _, err = io.Copy(destFile, sourceFile) - if err != nil { - return fmt.Errorf("failed to copy contents: %w", err) - } - // Flush - _ = destFile.Sync() } break new @@ -108,8 +118,7 @@ func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, ne func (bundle Bundle) DownloadAndVerify(dir string) { client := http.Client{} for _, artifact := range bundle.Artifacts { - - filePath := fmt.Sprintf("%s/%s", dir, artifact.Filename) + filePath := filepath.Join(dir, artifact.Filename) // TODO(vladimir): is this needed? _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode) @@ -131,13 +140,7 @@ func (bundle Bundle) DownloadAndVerify(dir string) { // Verify checks if the files are present int the dataDir and have the correct hash. func (bundle Bundle) Verify(dir string) error { for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", dir, artifact.Filename) - file, err := os.Open(artifactPath) - if err != nil { - return fmt.Errorf("failed to open file %s: %w", artifactPath, err) - } - defer func() { _ = file.Close() }() - + artifactPath := filepath.Join(dir, artifact.Filename) isValid, err := checkIfFileIsValid(artifactPath, artifact) if err != nil { return err @@ -177,11 +180,6 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } func processArtifact(client *http.Client, artifact Artifact, filePath string) error { - // Skip artifacts not meant for this machine. - if artifact.Platform != "" && artifact.Platform != currentPlatform { - return nil - } - providedHash, err := hex.DecodeString(artifact.SHA256) if err != nil || len(providedHash) != sha256.Size { return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) @@ -211,20 +209,14 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er // Save tmpFilename := fmt.Sprintf("%s.download", filePath) - file, err := os.Create(tmpFilename) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) + fileMode := defaultFileMode + if artifact.Platform == currentPlatform { + fileMode = executableFileMode } - if artifact.Platform == "" { - _ = file.Chmod(defaultFileMode) - } else { - _ = file.Chmod(executableFileMode) - } - _, err = file.Write(content) + err = os.WriteFile(tmpFilename, content, fileMode) if err != nil { return fmt.Errorf("failed to write to file: %w", err) } - file.Close() // Rename err = os.Rename(tmpFilename, filePath) @@ -265,17 +257,11 @@ func downloadFile(client *http.Client, urls []string) ([]byte, error) { func unpack(cType string, fileBytes []byte) ([]byte, error) { switch cType { case "zip": - { - return decompressZip(fileBytes) - } + return decompressZip(fileBytes) case "gz": - { - return decompressGzip(fileBytes) - } + return decompressGzip(fileBytes) default: - { - return nil, fmt.Errorf("unsupported compression type") - } + return nil, fmt.Errorf("unsupported compression type") } } diff --git a/service/updates/index.go b/service/updates/index.go index 76b34000..19713064 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -42,7 +42,7 @@ func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { } defer func() { _ = resp.Body.Close() }() filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) - file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode) + file, err := os.Create(filePath) if err != nil { return err } diff --git a/service/updates/module.go b/service/updates/module.go index e3976c9c..ac15d1f8 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -14,6 +14,8 @@ import ( "github.com/safing/portmaster/service/mgr" ) +const updateAvailableNotificationID = "updates:update-available" + type File struct { id string path string @@ -65,24 +67,15 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), updateIndex: index, + files: make(map[string]File), instance: instance, } // Events module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil) - module.updateCheckWorkerMgr.Repeat(30 * time.Second) - module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", func(w *mgr.WorkerCtx) error { - err := applyUpdates(module.updateIndex, *module.updateBundle) - if err != nil { - // TODO(vladimir): Send notification to UI - log.Errorf("updates: failed to apply updates: %s", err) - } else { - // TODO(vladimir): Prompt user to restart? - module.instance.Restart() - } - return nil - }, nil) + module.updateCheckWorkerMgr.Repeat(1 * time.Hour) + module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", module.applyUpdates, nil) var err error module.bundle, err = ParseBundle(module.updateIndex.Directory, module.updateIndex.IndexFile) @@ -92,18 +85,44 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { // Add bundle artifacts to registry. module.processBundle(module.bundle) + err = module.registerEndpoints() + if err != nil { + log.Errorf("failed to register endpoints: %s", err) + } - // Remove old files - m.Go("old files cleaner", func(ctx *mgr.WorkerCtx) error { - err := os.RemoveAll(module.updateIndex.PurgeDirectory) - if err != nil { - return fmt.Errorf("failed to delete folder: %w", err) - } - return nil - }) return module, nil } +func (u *Updates) registerEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Name: "Check for update", + Description: "Trigger update check", + Path: "updates/check", + Read: api.PermitAnyone, + ActionFunc: func(ar *api.Request) (msg string, err error) { + u.updateCheckWorkerMgr.Go() + return "Check for updates triggered", nil + }, + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Name: "Apply update", + Description: "Triggers update", + Path: "updates/apply", + Read: api.PermitAnyone, + ActionFunc: func(ar *api.Request) (msg string, err error) { + u.upgraderWorkerMgr.Go() + return "Apply updates triggered", nil + }, + }); err != nil { + return err + } + + return nil +} + func (reg *Updates) processBundle(bundle *Bundle) { for _, artifact := range bundle.Artifacts { artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) @@ -119,33 +138,52 @@ func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { u.updateBundle, err = ParseBundle(u.updateIndex.DownloadDirectory, u.updateIndex.IndexFile) if err != nil { - return fmt.Errorf("failed parse bundle: %s", err) + return fmt.Errorf("failed parsing bundle: %s", err) } defer u.EventResourcesUpdated.Submit(struct{}{}) - // Compare current and downloaded index version. - currentVersion, err := semver.NewVersion(u.bundle.Version) - downloadVersion, err := semver.NewVersion(u.updateBundle.Version) - if currentVersion.Compare(downloadVersion) <= 0 { - // no updates - log.Info("updates: check complete: no new updates") + hasUpdate, err := u.checkVersionIncrement() + if err != nil { + return fmt.Errorf("failed to compare versions: %s", err) + } + + if !hasUpdate { + log.Infof("updates: check compete: no new updates") return nil } log.Infof("updates: check complete: downloading new version: %s %s", u.updateBundle.Name, u.updateBundle.Version) - err = u.DownloadUpdates() + err = u.downloadUpdates() if err != nil { log.Errorf("updates: failed to download bundle: %s", err) - } else if u.updateIndex.AutoApply { - u.ApplyUpdates() + } else { + notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{ + ID: "apply", + Text: "Apply", + Type: notifications.ActionTypeInjectEvent, + Payload: "apply-updates", + }) } return nil } -// DownloadUpdates downloads available binary updates. -func (u *Updates) DownloadUpdates() error { +func (u *Updates) checkVersionIncrement() (bool, error) { + // Compare current and downloaded index version. + currentVersion, err := semver.NewVersion(u.bundle.Version) + if err != nil { + return false, err + } + downloadVersion, err := semver.NewVersion(u.updateBundle.Version) + if err != nil { + return false, err + } + log.Debugf("updates: checking version: curr: %s new: %s", currentVersion.String(), downloadVersion.String()) + return downloadVersion.GreaterThan(currentVersion), nil +} + +func (u *Updates) downloadUpdates() error { if u.updateBundle == nil { - // CheckForBinaryUpdates needs to be called before this. + // checkForUpdates needs to be called before this. return fmt.Errorf("no valid update bundle found") } _ = deleteUnfinishedDownloads(u.updateIndex.DownloadDirectory) @@ -157,7 +195,40 @@ func (u *Updates) DownloadUpdates() error { return nil } -func (u *Updates) ApplyUpdates() { +func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error { + // Check if we have new version + hasNewVersion, err := u.checkVersionIncrement() + if err != nil { + return fmt.Errorf("error while reading bundle version: %w", err) + } + + if !hasNewVersion { + return fmt.Errorf("there is no new version to apply") + } + + err = u.updateBundle.Verify(u.updateIndex.DownloadDirectory) + if err != nil { + return fmt.Errorf("failed to apply update: %s", err) + } + + err = switchFolders(u.updateIndex, *u.updateBundle) + if err != nil { + // TODO(vladimir): Send notification to UI + log.Errorf("updates: failed to apply updates: %s", err) + } else { + // TODO(vladimir): Prompt user to restart? + u.instance.Restart() + } + return nil +} + +// TriggerUpdateCheck triggers an update check +func (u *Updates) TriggerUpdateCheck() { + u.updateCheckWorkerMgr.Go() +} + +// TriggerApplyUpdates triggers upgrade +func (u *Updates) TriggerApplyUpdates() { u.upgraderWorkerMgr.Go() } @@ -173,7 +244,16 @@ func (u *Updates) Manager() *mgr.Manager { // Start starts the module. func (u *Updates) Start() error { + // Remove old files + u.m.Go("old files cleaner", func(ctx *mgr.WorkerCtx) error { + err := os.RemoveAll(u.updateIndex.PurgeDirectory) + if err != nil { + return fmt.Errorf("failed to delete folder: %w", err) + } + return nil + }) u.updateCheckWorkerMgr.Go() + return nil } diff --git a/service/updates/updater.go b/service/updates/updater.go index 736290e6..3c6b2892 100644 --- a/service/updates/updater.go +++ b/service/updates/updater.go @@ -15,7 +15,7 @@ const ( defaultDirMode = os.FileMode(0o0755) ) -func applyUpdates(updateIndex UpdateIndex, newBundle Bundle) error { +func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { // Create purge dir. err := os.MkdirAll(updateIndex.PurgeDirectory, defaultDirMode) if err != nil { @@ -30,17 +30,17 @@ func applyUpdates(updateIndex UpdateIndex, newBundle Bundle) error { // Move current version files into purge folder. for _, file := range files { - filepath := fmt.Sprintf("%s/%s", updateIndex.Directory, file.Name()) - purgePath := fmt.Sprintf("%s/%s", updateIndex.PurgeDirectory, file.Name()) - err := os.Rename(filepath, purgePath) + currentFilepath := filepath.Join(updateIndex.Directory, file.Name()) + purgePath := filepath.Join(updateIndex.PurgeDirectory, file.Name()) + err := os.Rename(currentFilepath, purgePath) if err != nil { - return fmt.Errorf("failed to move file %s: %w", filepath, err) + return fmt.Errorf("failed to move file %s: %w", currentFilepath, err) } } // Move the new index file - indexFile := fmt.Sprintf("%s/%s", updateIndex.DownloadDirectory, updateIndex.IndexFile) - newIndexFile := fmt.Sprintf("%s/%s", updateIndex.Directory, updateIndex.IndexFile) + indexFile := filepath.Join(updateIndex.DownloadDirectory, updateIndex.IndexFile) + newIndexFile := filepath.Join(updateIndex.Directory, updateIndex.IndexFile) err = os.Rename(indexFile, newIndexFile) if err != nil { return fmt.Errorf("failed to move index file %s: %w", indexFile, err) @@ -48,8 +48,8 @@ func applyUpdates(updateIndex UpdateIndex, newBundle Bundle) error { // Move downloaded files to the current version folder. for _, artifact := range newBundle.Artifacts { - fromFilepath := fmt.Sprintf("%s/%s", updateIndex.DownloadDirectory, artifact.Filename) - toFilepath := fmt.Sprintf("%s/%s", updateIndex.Directory, artifact.Filename) + fromFilepath := filepath.Join(updateIndex.DownloadDirectory, artifact.Filename) + toFilepath := filepath.Join(updateIndex.Directory, artifact.Filename) err = os.Rename(fromFilepath, toFilepath) if err != nil { return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) @@ -64,12 +64,12 @@ func deleteUnfinishedDownloads(rootDir string) error { return err } - // Check if the current file has the specified extension + // Check if the current file has the download extension if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { - log.Warningf("updates: deleting unfinished: %s\n", path) + log.Warningf("updates: deleting unfinished download file: %s\n", path) err := os.Remove(path) if err != nil { - return fmt.Errorf("failed to delete file %s: %w", path, err) + log.Errorf("updates: failed to delete unfinished download file %s: %w", path, err) } } From 072d7e69710166be8f94380ff5aecb48f12db977 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 19 Sep 2024 08:45:50 +0300 Subject: [PATCH 13/62] [WIP] Minor improvments --- service/updates/bundle.go | 14 ++++++-------- service/updates/index.go | 19 ++++++++++++++----- service/updates/module.go | 10 ++++++---- service/updates/updater.go | 20 ++++++++++---------- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/service/updates/bundle.go b/service/updates/bundle.go index c2de5a92..e590446d 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -115,12 +115,12 @@ func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, ne return nil } -func (bundle Bundle) DownloadAndVerify(dir string) { - client := http.Client{} +func (bundle Bundle) DownloadAndVerify(client *http.Client, dir string) { + // Make sure dir exists + _ = os.MkdirAll(dir, defaultDirMode) + for _, artifact := range bundle.Artifacts { filePath := filepath.Join(dir, artifact.Filename) - // TODO(vladimir): is this needed? - _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode) // Check file is already downloaded and valid. exists, _ := checkIfFileIsValid(filePath, artifact) @@ -130,7 +130,7 @@ func (bundle Bundle) DownloadAndVerify(dir string) { } // Download artifact - err := processArtifact(&client, artifact, filePath) + err := processArtifact(client, artifact, filePath) if err != nil { log.Errorf("updates: %s", err) } @@ -202,9 +202,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er // Verify hash := sha256.Sum256(content) if !bytes.Equal(providedHash, hash[:]) { - // FIXME(vladimir): just for testing. Make it an error. - err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename) - log.Debugf("updates: %s", err) + return fmt.Errorf("failed to verify artifact: %s", artifact.Filename) } // Save diff --git a/service/updates/index.go b/service/updates/index.go index 19713064..b753450c 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "os" + "path/filepath" "github.com/safing/portmaster/base/log" ) @@ -19,10 +20,11 @@ type UpdateIndex struct { AutoApply bool } -func (ui *UpdateIndex) DownloadIndexFile() (err error) { +func (ui *UpdateIndex) DownloadIndexFile(client *http.Client) (err error) { + // Make sure dir exists _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) for _, url := range ui.IndexURLs { - err = ui.downloadIndexFileFromURL(url) + err = ui.downloadIndexFileFromURL(client, url) if err != nil { log.Warningf("updates: failed while downloading index file %s", err) continue @@ -34,20 +36,27 @@ func (ui *UpdateIndex) DownloadIndexFile() (err error) { return } -func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error { - client := http.Client{} +func (ui *UpdateIndex) downloadIndexFileFromURL(client *http.Client, url string) error { + // Request the index file resp, err := client.Get(url) if err != nil { return fmt.Errorf("failed GET request to %s: %w", url, err) } defer func() { _ = resp.Body.Close() }() - filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile) + + // Check the status code + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("received error from the server status code: %s", resp.Status) + } + // Create file + filePath := filepath.Join(ui.DownloadDirectory, ui.IndexFile) file, err := os.Create(filePath) if err != nil { return err } defer func() { _ = file.Close() }() + // Write body of the response _, err = io.Copy(file, resp.Body) if err != nil { return err diff --git a/service/updates/module.go b/service/updates/module.go index ac15d1f8..99c110da 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -3,6 +3,7 @@ package updates import ( "errors" "fmt" + "net/http" "os" "time" @@ -131,7 +132,8 @@ func (reg *Updates) processBundle(bundle *Bundle) { } func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { - err := u.updateIndex.DownloadIndexFile() + httpClient := http.Client{} + err := u.updateIndex.DownloadIndexFile(&httpClient) if err != nil { return fmt.Errorf("failed to download index file: %s", err) } @@ -153,7 +155,7 @@ func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { } log.Infof("updates: check complete: downloading new version: %s %s", u.updateBundle.Name, u.updateBundle.Version) - err = u.downloadUpdates() + err = u.downloadUpdates(&httpClient) if err != nil { log.Errorf("updates: failed to download bundle: %s", err) } else { @@ -181,7 +183,7 @@ func (u *Updates) checkVersionIncrement() (bool, error) { return downloadVersion.GreaterThan(currentVersion), nil } -func (u *Updates) downloadUpdates() error { +func (u *Updates) downloadUpdates(client *http.Client) error { if u.updateBundle == nil { // checkForUpdates needs to be called before this. return fmt.Errorf("no valid update bundle found") @@ -191,7 +193,7 @@ func (u *Updates) downloadUpdates() error { if err != nil { log.Warningf("updates: error while coping file from current to update: %s", err) } - u.updateBundle.DownloadAndVerify(u.updateIndex.DownloadDirectory) + u.updateBundle.DownloadAndVerify(client, u.updateIndex.DownloadDirectory) return nil } diff --git a/service/updates/updater.go b/service/updates/updater.go index 3c6b2892..4a0358ec 100644 --- a/service/updates/updater.go +++ b/service/updates/updater.go @@ -59,20 +59,20 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { } func deleteUnfinishedDownloads(rootDir string) error { - return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - + entries, err := os.ReadDir(rootDir) + if err != nil { + return err + } + for _, e := range entries { // Check if the current file has the download extension - if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".download") { + path := filepath.Join(rootDir, e.Name()) log.Warningf("updates: deleting unfinished download file: %s\n", path) err := os.Remove(path) if err != nil { - log.Errorf("updates: failed to delete unfinished download file %s: %w", path, err) + log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err) } } - - return nil - }) + } + return nil } From b3ff6f14f173b5ce412b12baf81facfb984a8958 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 19 Sep 2024 12:38:23 +0300 Subject: [PATCH 14/62] [WIP] Error and state handleing improvments, better logs --- service/core/api.go | 14 +- service/firewall/api.go | 4 +- service/instance.go | 6 +- service/process/module.go | 8 - service/process/profile.go | 27 +- service/profile/module.go | 12 +- service/updates/api.go | 161 --------- service/updates/bundle.go | 26 +- service/updates/config.go | 178 ---------- service/updates/export.go | 237 ------------- service/updates/get.go | 65 ---- service/updates/helper/electron.go | 58 ---- service/updates/helper/indexes.go | 136 -------- service/updates/helper/signing.go | 42 --- service/updates/helper/updates.go | 95 ----- service/updates/index.go | 19 +- service/updates/main.go | 69 ---- service/updates/module.go | 102 +++--- service/updates/notify.go | 176 ---------- service/updates/os_integration_default.go | 8 - service/updates/os_integration_linux.go | 201 ----------- service/updates/state.go | 49 --- service/updates/updater.go | 31 +- service/updates/upgrader.go | 403 ---------------------- 24 files changed, 146 insertions(+), 1981 deletions(-) delete mode 100644 service/updates/api.go delete mode 100644 service/updates/config.go delete mode 100644 service/updates/export.go delete mode 100644 service/updates/get.go delete mode 100644 service/updates/helper/electron.go delete mode 100644 service/updates/helper/indexes.go delete mode 100644 service/updates/helper/signing.go delete mode 100644 service/updates/helper/updates.go delete mode 100644 service/updates/notify.go delete mode 100644 service/updates/os_integration_default.go delete mode 100644 service/updates/os_integration_linux.go delete mode 100644 service/updates/state.go delete mode 100644 service/updates/upgrader.go diff --git a/service/core/api.go b/service/core/api.go index aa1305e0..2c465ccc 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -107,27 +107,29 @@ func registerAPIEndpoints() error { } if err := api.RegisterEndpoint(api.Endpoint{ - Path: "updates/check", - Read: api.PermitUser, + Path: "updates/check", + WriteMethod: "POST", + Write: api.PermitUser, ActionFunc: func(ar *api.Request) (string, error) { module.instance.BinaryUpdates().TriggerUpdateCheck() module.instance.IntelUpdates().TriggerUpdateCheck() return "update check triggered", nil }, - Name: "Get the ID of the calling profile", + Name: "Trigger updates check event", }); err != nil { return err } if err := api.RegisterEndpoint(api.Endpoint{ - Path: "updates/apply", - Read: api.PermitUser, + Path: "updates/apply", + WriteMethod: "POST", + Write: api.PermitUser, ActionFunc: func(ar *api.Request) (string, error) { module.instance.BinaryUpdates().TriggerApplyUpdates() module.instance.IntelUpdates().TriggerApplyUpdates() return "upgrade triggered", nil }, - Name: "Get the ID of the calling profile", + Name: "Trigger updates apply event", }); err != nil { return err } diff --git a/service/firewall/api.go b/service/firewall/api.go index 134f6f74..f16b7acd 100644 --- a/service/firewall/api.go +++ b/service/firewall/api.go @@ -18,7 +18,6 @@ import ( "github.com/safing/portmaster/service/network/netutils" "github.com/safing/portmaster/service/network/packet" "github.com/safing/portmaster/service/process" - "github.com/safing/portmaster/service/updates" ) const ( @@ -133,7 +132,8 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo var originalPid int // Get authenticated path. - authenticatedPath := updates.RootPath() + // FIXME(vladimir): provide a better check for detecting filepath. Note there is exception on linux with portmaster ui. + authenticatedPath := "" // updates.RootPath() if authenticatedPath == "" { return false, fmt.Errorf(deniedMsgMisconfigured, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user } diff --git a/service/instance.go b/service/instance.go index c514fd80..f687062b 100644 --- a/service/instance.go +++ b/service/instance.go @@ -133,13 +133,14 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx return nil, err } binaryUpdateIndex = updates.UpdateIndex{ - Directory: binaryFolder, // Default: C:/Program Files/Portmaster/binary + Directory: binaryFolder, // Default: C:/Program Files/Portmaster DownloadDirectory: os.ExpandEnv("%ProgramData%/Portmaster/new_binary"), PurgeDirectory: os.ExpandEnv("%ProgramData%/Portmaster/old_binary"), Ignore: []string{"databases", "intel", "config.json"}, IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, IndexFile: "bin-index.json", AutoApply: false, + NeedsRestart: true, } intelUpdateIndex = updates.UpdateIndex{ @@ -149,6 +150,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, + NeedsRestart: false, } } else if go_runtime.GOOS == "linux" { binaryUpdateIndex = updates.UpdateIndex{ @@ -159,6 +161,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx IndexURLs: []string{"http://localhost:8000/test-binary.json"}, IndexFile: "bin-index.json", AutoApply: false, + NeedsRestart: true, } intelUpdateIndex = updates.UpdateIndex{ @@ -168,6 +171,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, + NeedsRestart: false, } } diff --git a/service/process/module.go b/service/process/module.go index 563368ab..b17a0c05 100644 --- a/service/process/module.go +++ b/service/process/module.go @@ -2,11 +2,9 @@ package process import ( "errors" - "os" "sync/atomic" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" ) type ProcessModule struct { @@ -19,10 +17,6 @@ func (pm *ProcessModule) Manager() *mgr.Manager { } func (pm *ProcessModule) Start() error { - updatesPath = updates.RootPath() - if updatesPath != "" { - updatesPath += string(os.PathSeparator) - } return nil } @@ -30,8 +24,6 @@ func (pm *ProcessModule) Stop() error { return nil } -var updatesPath string - func prep() error { if err := registerConfiguration(); err != nil { return err diff --git a/service/process/profile.go b/service/process/profile.go index e8c766ee..7ac4ed15 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -72,19 +72,20 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) { specialProfileID = profile.PortmasterProfileID default: // Check if this is another Portmaster component. - if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) { - switch { - case strings.Contains(p.Path, "portmaster-app"): - specialProfileID = profile.PortmasterAppProfileID - case strings.Contains(p.Path, "portmaster-notifier"): - specialProfileID = profile.PortmasterNotifierProfileID - default: - // Unexpected binary from within the Portmaster updates directpry. - log.Warningf("process: unexpected binary in the updates directory: %s", p.Path) - // TODO: Assign a fully restricted profile in the future when we are - // sure that we won't kill any of our own things. - } - } + // FIXME(vladimir): provide a better check for detecting filepath. Note there is exception on linux with portmaster ui. + // if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) { + // switch { + // case strings.Contains(p.Path, "portmaster-app"): + // specialProfileID = profile.PortmasterAppProfileID + // case strings.Contains(p.Path, "portmaster-notifier"): + // specialProfileID = profile.PortmasterNotifierProfileID + // default: + // // Unexpected binary from within the Portmaster updates directpry. + // log.Warningf("process: unexpected binary in the updates directory: %s", p.Path) + // // TODO: Assign a fully restricted profile in the future when we are + // // sure that we won't kill any of our own things. + // } + // } // Check if this is the system resolver. switch runtime.GOOS { case "windows": diff --git a/service/profile/module.go b/service/profile/module.go index 911ef99c..67873b8c 100644 --- a/service/profile/module.go +++ b/service/profile/module.go @@ -3,7 +3,6 @@ package profile import ( "errors" "fmt" - "os" "sync/atomic" "github.com/safing/portmaster/base/config" @@ -14,13 +13,9 @@ import ( _ "github.com/safing/portmaster/service/core/base" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/profile/binmeta" - "github.com/safing/portmaster/service/updates" ) -var ( - migrations = migration.New("core:migrations/profile") - updatesPath string -) +var migrations = migration.New("core:migrations/profile") // Events. const ( @@ -80,11 +75,6 @@ func prep() error { } func start() error { - updatesPath = updates.RootPath() - if updatesPath != "" { - updatesPath += string(os.PathSeparator) - } - if err := loadProfilesMetadata(); err != nil { if !errors.Is(err, database.ErrNotFound) { log.Warningf("profile: failed to load profiles metadata, falling back to empty state: %s", err) diff --git a/service/updates/api.go b/service/updates/api.go deleted file mode 100644 index 6c4dbf0c..00000000 --- a/service/updates/api.go +++ /dev/null @@ -1,161 +0,0 @@ -package updates - -import ( -// "bytes" -// "io" -// "net/http" -// "os" -// "path/filepath" -// "strings" - -// "github.com/ghodss/yaml" - -// "github.com/safing/portmaster/base/api" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -) - -const ( - apiPathCheckForUpdates = "updates/check" -) - -// func registerAPIEndpoints() error { -// if err := api.RegisterEndpoint(api.Endpoint{ -// Name: "Check for Updates", -// Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", -// Parameters: []api.Parameter{{ -// Method: http.MethodPost, -// Field: "download", -// Value: "", -// Description: "Force downloading and applying of all updates, regardless of auto-update settings.", -// }}, -// Path: apiPathCheckForUpdates, -// Write: api.PermitUser, -// ActionFunc: func(r *api.Request) (msg string, err error) { -// // Check if we should also download regardless of settings. -// downloadAll := r.URL.Query().Has("download") - -// // Trigger update task. -// err = TriggerUpdate(true, downloadAll) -// if err != nil { -// return "", err -// } - -// // Report how we triggered. -// if downloadAll { -// return "downloading all updates...", nil -// } -// return "checking for updates...", nil -// }, -// }); err != nil { -// return err -// } - -// if err := api.RegisterEndpoint(api.Endpoint{ -// Name: "Get Resource", -// Description: "Returns the requested resource from the udpate system", -// Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, -// Read: api.PermitUser, -// ReadMethod: http.MethodGet, -// HandlerFunc: func(w http.ResponseWriter, r *http.Request) { -// // Get identifier from URL. -// var identifier string -// if ar := api.GetAPIRequest(r); ar != nil { -// identifier = ar.URLVars["identifier"] -// } -// if identifier == "" { -// http.Error(w, "no resource speicified", http.StatusBadRequest) -// return -// } - -// // Get resource. -// resource, err := registry.GetFile(identifier) -// if err != nil { -// http.Error(w, err.Error(), http.StatusNotFound) -// return -// } - -// // Open file for reading. -// file, err := os.Open(resource.Path()) -// if err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } -// defer file.Close() //nolint:errcheck,gosec - -// // Assign file to reader -// var reader io.Reader = file - -// // Add version to header. -// w.Header().Set("Resource-Version", resource.Version()) - -// // Set Content-Type. -// contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) -// w.Header().Set("Content-Type", contentType) - -// // Check if the content type may be returned. -// accept := r.Header.Get("Accept") -// if accept != "" { -// mimeTypes := strings.Split(accept, ",") -// // First, clean mime types. -// for i, mimeType := range mimeTypes { -// mimeType = strings.TrimSpace(mimeType) -// mimeType, _, _ = strings.Cut(mimeType, ";") -// mimeTypes[i] = mimeType -// } -// // Second, check if we may return anything. -// var acceptsAny bool -// for _, mimeType := range mimeTypes { -// switch mimeType { -// case "*", "*/*": -// acceptsAny = true -// } -// } -// // Third, check if we can convert. -// if !acceptsAny { -// var converted bool -// sourceType, _, _ := strings.Cut(contentType, ";") -// findConvertiblePair: -// for _, mimeType := range mimeTypes { -// switch { -// case sourceType == "application/yaml" && mimeType == "application/json": -// yamlData, err := io.ReadAll(reader) -// if err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } -// jsonData, err := yaml.YAMLToJSON(yamlData) -// if err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } -// reader = bytes.NewReader(jsonData) -// converted = true -// break findConvertiblePair -// } -// } - -// // If we could not convert to acceptable format, return an error. -// if !converted { -// http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) -// return -// } -// } -// } - -// // Write file. -// w.WriteHeader(http.StatusOK) -// if r.Method != http.MethodHead { -// _, err = io.Copy(w, reader) -// if err != nil { -// log.Errorf("updates: failed to serve resource file: %s", err) -// return -// } -// } -// }, -// }); err != nil { -// return err -// } - -// return nil -// } diff --git a/service/updates/bundle.go b/service/updates/bundle.go index e590446d..904ae8fd 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "compress/gzip" + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -106,7 +107,7 @@ func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, ne if err != nil { return fmt.Errorf("failed to write to file %s: %w", destFilePath, err) } - + log.Debugf("updates: file copied from current version: %s", newArtifact.Filename) } break new } @@ -115,7 +116,7 @@ func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, ne return nil } -func (bundle Bundle) DownloadAndVerify(client *http.Client, dir string) { +func (bundle Bundle) DownloadAndVerify(ctx context.Context, client *http.Client, dir string) { // Make sure dir exists _ = os.MkdirAll(dir, defaultDirMode) @@ -130,7 +131,7 @@ func (bundle Bundle) DownloadAndVerify(client *http.Client, dir string) { } // Download artifact - err := processArtifact(client, artifact, filePath) + err := processArtifact(ctx, client, artifact, filePath) if err != nil { log.Errorf("updates: %s", err) } @@ -179,14 +180,15 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { return true, nil } -func processArtifact(client *http.Client, artifact Artifact, filePath string) error { +func processArtifact(ctx context.Context, client *http.Client, artifact Artifact, filePath string) error { providedHash, err := hex.DecodeString(artifact.SHA256) if err != nil || len(providedHash) != sha256.Size { return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) } // Download - content, err := downloadFile(client, artifact.URLs) + log.Debugf("updates: downloading file: %s", artifact.Filename) + content, err := downloadFile(ctx, client, artifact.URLs) if err != nil { return fmt.Errorf("failed to download artifact: %w", err) } @@ -222,13 +224,23 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er return fmt.Errorf("failed to rename file: %w", err) } + log.Infof("updates: file downloaded and verified: %s", artifact.Filename) + return nil } -func downloadFile(client *http.Client, urls []string) ([]byte, error) { +func downloadFile(ctx context.Context, client *http.Client, urls []string) ([]byte, error) { for _, url := range urls { // Try to make the request - resp, err := client.Get(url) + req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) + if err != nil { + log.Warningf("failed to create GET request to %s: %s", url, err) + continue + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + resp, err := client.Do(req) if err != nil { log.Warningf("failed a get file request to: %s", err) continue diff --git a/service/updates/config.go b/service/updates/config.go deleted file mode 100644 index e563d557..00000000 --- a/service/updates/config.go +++ /dev/null @@ -1,178 +0,0 @@ -package updates - -import ( - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/config" - // "github.com/safing/portmaster/base/log" - // "github.com/safing/portmaster/service/mgr" - // "github.com/safing/portmaster/service/updates/helper" -) - -const cfgDevModeKey = "core/devMode" - -var ( - releaseChannel config.StringOption - devMode config.BoolOption - enableSoftwareUpdates config.BoolOption - enableIntelUpdates config.BoolOption - - initialReleaseChannel string - previousReleaseChannel string - - softwareUpdatesCurrentlyEnabled bool - intelUpdatesCurrentlyEnabled bool - previousDevMode bool - forceCheck = abool.New() - forceDownload = abool.New() -) - -// func registerConfig() error { -// err := config.Register(&config.Option{ -// Name: "Release Channel", -// Key: helper.ReleaseChannelKey, -// Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, -// OptType: config.OptTypeString, -// ExpertiseLevel: config.ExpertiseLevelExpert, -// ReleaseLevel: config.ReleaseLevelStable, -// RequiresRestart: true, -// DefaultValue: helper.ReleaseChannelStable, -// PossibleValues: []config.PossibleValue{ -// { -// Name: "Stable", -// Description: "Production releases.", -// Value: helper.ReleaseChannelStable, -// }, -// { -// Name: "Beta", -// Description: "Production releases for testing new features that may break and cause interruption.", -// Value: helper.ReleaseChannelBeta, -// }, -// { -// Name: "Support", -// Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", -// Value: helper.ReleaseChannelSupport, -// }, -// { -// Name: "Staging", -// Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", -// Value: helper.ReleaseChannelStaging, -// }, -// }, -// Annotations: config.Annotations{ -// config.DisplayOrderAnnotation: -4, -// config.DisplayHintAnnotation: config.DisplayHintOneOf, -// config.CategoryAnnotation: "Updates", -// }, -// }) -// if err != nil { -// return err -// } - -// err = config.Register(&config.Option{ -// Name: "Automatic Software Updates", -// Key: enableSoftwareUpdatesKey, -// Description: "Automatically check for and download software updates. This does not include intelligence data updates.", -// OptType: config.OptTypeBool, -// ExpertiseLevel: config.ExpertiseLevelExpert, -// ReleaseLevel: config.ReleaseLevelStable, -// RequiresRestart: false, -// DefaultValue: true, -// Annotations: config.Annotations{ -// config.DisplayOrderAnnotation: -12, -// config.CategoryAnnotation: "Updates", -// }, -// }) -// if err != nil { -// return err -// } - -// err = config.Register(&config.Option{ -// Name: "Automatic Intelligence Data Updates", -// Key: enableIntelUpdatesKey, -// Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", -// OptType: config.OptTypeBool, -// ExpertiseLevel: config.ExpertiseLevelExpert, -// ReleaseLevel: config.ReleaseLevelStable, -// RequiresRestart: false, -// DefaultValue: true, -// Annotations: config.Annotations{ -// config.DisplayOrderAnnotation: -11, -// config.CategoryAnnotation: "Updates", -// }, -// }) -// if err != nil { -// return err -// } - -// return nil -// } - -// func initConfig() { -// releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) -// initialReleaseChannel = releaseChannel() -// previousReleaseChannel = releaseChannel() - -// enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) -// enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) -// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() -// intelUpdatesCurrentlyEnabled = enableIntelUpdates() - -// devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false) -// previousDevMode = devMode() -// } - -// func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { -// changed := false - -// if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { -// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() -// changed = true -// } - -// if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { -// intelUpdatesCurrentlyEnabled = enableIntelUpdates() -// changed = true -// } - -// if devMode() != previousDevMode { -// registry.SetDevMode(devMode()) -// previousDevMode = devMode() -// changed = true -// } - -// if releaseChannel() != previousReleaseChannel { -// previousReleaseChannel = releaseChannel() -// changed = true -// } - -// if changed { -// // Update indexes based on new settings. -// warning := helper.SetIndexes( -// registry, -// releaseChannel(), -// true, -// softwareUpdatesCurrentlyEnabled, -// intelUpdatesCurrentlyEnabled, -// ) -// if warning != nil { -// log.Warningf("updates: %s", warning) -// } - -// // Select versions depending on new indexes and modes. -// registry.SelectVersions() -// module.EventVersionsUpdated.Submit(struct{}{}) - -// if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { -// module.states.Clear() -// if err := TriggerUpdate(true, false); err != nil { -// log.Warningf("updates: failed to trigger update: %s", err) -// } -// log.Infof("updates: automatic updates are now enabled") -// } else { -// log.Warningf("updates: automatic updates are now completely disabled") -// } -// } - -// return false, nil -// } diff --git a/service/updates/export.go b/service/updates/export.go deleted file mode 100644 index c736855c..00000000 --- a/service/updates/export.go +++ /dev/null @@ -1,237 +0,0 @@ -package updates - -// import ( -// "fmt" -// "sort" -// "sync" - -// "github.com/safing/portmaster/base/database/record" -// "github.com/safing/portmaster/base/info" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/updater" -// "github.com/safing/portmaster/base/utils/debug" -// "github.com/safing/portmaster/service/mgr" -// "github.com/safing/portmaster/service/updates/helper" -// ) - -// const ( -// // versionsDBKey is the database key for update version information. -// versionsDBKey = "core:status/versions" - -// // versionsDBKey is the database key for simple update version information. -// simpleVersionsDBKey = "core:status/simple-versions" - -// // updateStatusDBKey is the database key for update status information. -// updateStatusDBKey = "core:status/updates" -// ) - -// // Versions holds update versions and status information. -// type Versions struct { -// record.Base -// sync.Mutex - -// Core *info.Info -// Resources map[string]*updater.Resource -// Channel string -// Beta bool -// Staging bool -// } - -// // SimpleVersions holds simplified update versions and status information. -// type SimpleVersions struct { -// record.Base -// sync.Mutex - -// Build *info.Info -// Resources map[string]*SimplifiedResourceVersion -// Channel string -// } - -// // SimplifiedResourceVersion holds version information about one resource. -// type SimplifiedResourceVersion struct { -// Version string -// } - -// // UpdateStateExport is a wrapper to export the updates state. -// type UpdateStateExport struct { -// record.Base -// sync.Mutex - -// *updater.UpdateState -// } - -// // GetVersions returns the update versions and status information. -// // Resources must be locked when accessed. -// func GetVersions() *Versions { -// return &Versions{ -// Core: info.GetInfo(), -// Resources: nil, -// Channel: initialReleaseChannel, -// Beta: initialReleaseChannel == helper.ReleaseChannelBeta, -// Staging: initialReleaseChannel == helper.ReleaseChannelStaging, -// } -// } - -// // GetSimpleVersions returns the simplified update versions and status information. -// func GetSimpleVersions() *SimpleVersions { -// // Fill base info. -// v := &SimpleVersions{ -// Build: info.GetInfo(), -// Resources: make(map[string]*SimplifiedResourceVersion), -// Channel: initialReleaseChannel, -// } - -// // Iterate through all versions and add version info. -// // for id, resource := range registry.Export() { -// // func() { -// // resource.Lock() -// // defer resource.Unlock() - -// // // Get current in-used or selected version. -// // var rv *updater.ResourceVersion -// // switch { -// // case resource.ActiveVersion != nil: -// // rv = resource.ActiveVersion -// // case resource.SelectedVersion != nil: -// // rv = resource.SelectedVersion -// // } - -// // // Get information from resource. -// // if rv != nil { -// // v.Resources[id] = &SimplifiedResourceVersion{ -// // Version: rv.VersionNumber, -// // } -// // } -// // }() -// // } - -// return v -// } - -// // GetStateExport gets the update state from the registry and returns it in an -// // exportable struct. -// func GetStateExport() *UpdateStateExport { -// // export := registry.GetState() -// return &UpdateStateExport{ -// // UpdateState: &export.Updates, -// } -// } - -// // LoadStateExport loads the exported update state from the database. -// func LoadStateExport() (*UpdateStateExport, error) { -// r, err := db.Get(updateStatusDBKey) -// if err != nil { -// return nil, err -// } - -// // unwrap -// if r.IsWrapped() { -// // only allocate a new struct, if we need it -// newRecord := &UpdateStateExport{} -// err = record.Unwrap(r, newRecord) -// if err != nil { -// return nil, err -// } -// return newRecord, nil -// } - -// // or adjust type -// newRecord, ok := r.(*UpdateStateExport) -// if !ok { -// return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r) -// } -// return newRecord, nil -// } - -// func initVersionExport() (err error) { -// if err := GetVersions().save(); err != nil { -// log.Warningf("updates: failed to export version information: %s", err) -// } -// if err := GetSimpleVersions().save(); err != nil { -// log.Warningf("updates: failed to export version information: %s", err) -// } - -// // module.EventVersionsUpdated.AddCallback("export version status", export) -// return nil -// } - -// func (v *Versions) save() error { -// if !v.KeyIsSet() { -// v.SetKey(versionsDBKey) -// } -// return db.Put(v) -// } - -// func (v *SimpleVersions) save() error { -// if !v.KeyIsSet() { -// v.SetKey(simpleVersionsDBKey) -// } -// return db.Put(v) -// } - -// func (s *UpdateStateExport) save() error { -// if !s.KeyIsSet() { -// s.SetKey(updateStatusDBKey) -// } -// return db.Put(s) -// } - -// // export is an event hook. -// func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { -// // Export versions. -// if err := GetVersions().save(); err != nil { -// return false, err -// } -// if err := GetSimpleVersions().save(); err != nil { -// return false, err -// } -// // Export udpate state. -// if err := GetStateExport().save(); err != nil { -// return false, err -// } - -// return false, nil -// } - -// // AddToDebugInfo adds the update system status to the given debug.Info. -// func AddToDebugInfo(di *debug.Info) { -// // Get resources from registry. -// // resources := registry.Export() -// // platformPrefix := helper.PlatformIdentifier("") - -// // Collect data for debug info. -// var active, selected []string -// var activeCnt, totalCnt int -// // for id, r := range resources { -// // // Ignore resources for other platforms. -// // if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { -// // continue -// // } - -// // totalCnt++ -// // if r.ActiveVersion != nil { -// // activeCnt++ -// // active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) -// // } -// // if r.SelectedVersion != nil { -// // selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) -// // } -// // } -// sort.Strings(active) -// sort.Strings(selected) - -// // Compile to one list. -// lines := make([]string, 0, len(active)+len(selected)+3) -// lines = append(lines, "Active:") -// lines = append(lines, active...) -// lines = append(lines, "") -// lines = append(lines, "Selected:") -// lines = append(lines, selected...) - -// // Add section. -// di.AddSection( -// fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), -// debug.UseCodeSection|debug.AddContentLineBreaks, -// lines..., -// ) -// } diff --git a/service/updates/get.go b/service/updates/get.go deleted file mode 100644 index 75fc4c3e..00000000 --- a/service/updates/get.go +++ /dev/null @@ -1,65 +0,0 @@ -package updates - -// GetPlatformFile returns the latest platform specific file identified by the given identifier. -// func GetPlatformFile(identifier string) (*updater.File, error) { -// identifier = helper.PlatformIdentifier(identifier) - -// file, err := registry.GetFile(identifier) -// if err != nil { -// return nil, err -// } - -// module.EventVersionsUpdated.Submit(struct{}{}) -// return file, nil -// } - -// GetFile returns the latest generic file identified by the given identifier. -// func GetFile(identifier string) (*updater.File, error) { -// identifier = path.Join("all", identifier) - -// file, err := registry.GetFile(identifier) -// if err != nil { -// return nil, err -// } - -// module.EventVersionsUpdated.Submit(struct{}{}) -// return file, nil -// } - -// GetPlatformVersion returns the selected platform specific version of the -// given identifier. -// The returned resource version may not be modified. -// func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) { -// identifier = helper.PlatformIdentifier(identifier) - -// rv, err := registry.GetVersion(identifier) -// if err != nil { -// return nil, err -// } - -// return rv, nil -// } - -// GetVersion returns the selected generic version of the given identifier. -// The returned resource version may not be modified. -// func GetVersion(identifier string) (*updater.ResourceVersion, error) { -// identifier = path.Join("all", identifier) - -// rv, err := registry.GetVersion(identifier) -// if err != nil { -// return nil, err -// } - -// return rv, nil -// } - -// GetVersionWithFullID returns the selected generic version of the given full identifier. -// The returned resource version may not be modified. -// func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { -// rv, err := registry.GetVersion(identifier) -// if err != nil { -// return nil, err -// } - -// return rv, nil -// } diff --git a/service/updates/helper/electron.go b/service/updates/helper/electron.go deleted file mode 100644 index a0c10149..00000000 --- a/service/updates/helper/electron.go +++ /dev/null @@ -1,58 +0,0 @@ -package helper - -// import ( -// "errors" -// "fmt" -// "os" -// "path/filepath" -// "runtime" -// "strings" - -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/updater" -// "github.com/safing/portmaster/service/updates/registry" -// ) - -// var pmElectronUpdate *registry.File - -// const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` - -// // EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed -// // by our app-electron package has the SUID bit set on systems that do not -// // allow unprivileged CLONE_NEWUSER (clone(3)). -// // On non-linux systems or systems that have kernel.unprivileged_userns_clone -// // set to 1 EnsureChromeSandboPermissions is a NO-OP. -// func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { -// if runtime.GOOS != "linux" { -// return nil -// } - -// if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { -// return nil -// } - -// identifier := PlatformIdentifier("app/portmaster-app.zip") - -// var err error -// pmElectronUpdate, err = reg.GetFile(identifier) -// if err != nil { -// if errors.Is(err, updater.ErrNotAvailableLocally) { -// return nil -// } -// return fmt.Errorf("failed to get file: %w", err) -// } - -// unpackedPath := strings.TrimSuffix( -// pmElectronUpdate.Path(), -// filepath.Ext(pmElectronUpdate.Path()), -// ) -// sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") -// if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { -// log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) - -// return fmt.Errorf("failed to chmod: %w", err) -// } -// log.Debugf("updates: fixed SUID permission for chrome-sandbox") - -// return nil -// } diff --git a/service/updates/helper/indexes.go b/service/updates/helper/indexes.go deleted file mode 100644 index 7b9e671e..00000000 --- a/service/updates/helper/indexes.go +++ /dev/null @@ -1,136 +0,0 @@ -package helper - -// import ( -// "errors" -// "fmt" -// "io/fs" -// "os" -// "path/filepath" - -// "github.com/safing/jess/filesig" -// "github.com/safing/portmaster/base/updater" -// ) - -// // Release Channel Configuration Keys. -// const ( -// ReleaseChannelKey = "core/releaseChannel" -// ReleaseChannelJSONKey = "core.releaseChannel" -// ) - -// // Release Channels. -// const ( -// ReleaseChannelStable = "stable" -// ReleaseChannelBeta = "beta" -// ReleaseChannelStaging = "staging" -// ReleaseChannelSupport = "support" -// ) - -// const jsonSuffix = ".json" - -// // SetIndexes sets the update registry indexes and also configures the registry -// // to use pre-releases based on the channel. -// func SetIndexes( -// registry *updater.ResourceRegistry, -// releaseChannel string, -// deleteUnusedIndexes bool, -// autoDownload bool, -// autoDownloadIntel bool, -// ) (warning error) { -// usePreReleases := false - -// // Be reminded that the order is important, as indexes added later will -// // override the current release from earlier indexes. - -// // Reset indexes before adding them (again). -// registry.ResetIndexes() - -// // Add the intel index first, in order to be able to override it with the -// // other indexes when needed. -// registry.AddIndex(updater.Index{ -// Path: "all/intel/intel.json", -// AutoDownload: autoDownloadIntel, -// }) - -// // Always add the stable index as a base. -// registry.AddIndex(updater.Index{ -// Path: ReleaseChannelStable + jsonSuffix, -// AutoDownload: autoDownload, -// }) - -// // Add beta index if in beta or staging channel. -// indexPath := ReleaseChannelBeta + jsonSuffix -// if releaseChannel == ReleaseChannelBeta || -// releaseChannel == ReleaseChannelStaging || -// (releaseChannel == "" && indexExists(registry, indexPath)) { -// registry.AddIndex(updater.Index{ -// Path: indexPath, -// PreRelease: true, -// AutoDownload: autoDownload, -// }) -// usePreReleases = true -// } else if deleteUnusedIndexes { -// err := deleteIndex(registry, indexPath) -// if err != nil { -// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) -// } -// } - -// // Add staging index if in staging channel. -// indexPath = ReleaseChannelStaging + jsonSuffix -// if releaseChannel == ReleaseChannelStaging || -// (releaseChannel == "" && indexExists(registry, indexPath)) { -// registry.AddIndex(updater.Index{ -// Path: indexPath, -// PreRelease: true, -// AutoDownload: autoDownload, -// }) -// usePreReleases = true -// } else if deleteUnusedIndexes { -// err := deleteIndex(registry, indexPath) -// if err != nil { -// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) -// } -// } - -// // Add support index if in support channel. -// indexPath = ReleaseChannelSupport + jsonSuffix -// if releaseChannel == ReleaseChannelSupport || -// (releaseChannel == "" && indexExists(registry, indexPath)) { -// registry.AddIndex(updater.Index{ -// Path: indexPath, -// AutoDownload: autoDownload, -// }) -// usePreReleases = true -// } else if deleteUnusedIndexes { -// err := deleteIndex(registry, indexPath) -// if err != nil { -// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) -// } -// } - -// // Set pre-release usage. -// registry.SetUsePreReleases(usePreReleases) - -// return warning -// } - -// func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { -// _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) -// return err == nil -// } - -// func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { -// // Remove index itself. -// err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) -// if err != nil && !errors.Is(err, fs.ErrNotExist) { -// return err -// } - -// // Remove any accompanying signature. -// err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) -// if err != nil && !errors.Is(err, fs.ErrNotExist) { -// return err -// } - -// return nil -// } diff --git a/service/updates/helper/signing.go b/service/updates/helper/signing.go deleted file mode 100644 index 9af98699..00000000 --- a/service/updates/helper/signing.go +++ /dev/null @@ -1,42 +0,0 @@ -package helper - -// import ( -// "github.com/safing/jess" -// "github.com/safing/portmaster/base/updater" -// ) - -// var ( -// // VerificationConfig holds the complete verification configuration for the registry. -// VerificationConfig = map[string]*updater.VerificationOptions{ -// "": { // Default. -// TrustStore: BinarySigningTrustStore, -// DownloadPolicy: updater.SignaturePolicyRequire, -// DiskLoadPolicy: updater.SignaturePolicyWarn, -// }, -// "all/intel/": nil, // Disable until IntelHub supports signing. -// } - -// // BinarySigningKeys holds the signing keys in text format. -// BinarySigningKeys = []string{ -// // Safing Code Signing Key #1 -// "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", -// // Safing Code Signing Key #2 -// "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", -// } - -// // BinarySigningTrustStore is an in-memory trust store with the signing keys. -// BinarySigningTrustStore = jess.NewMemTrustStore() -// ) - -// func init() { -// for _, signingKey := range BinarySigningKeys { -// rcpt, err := jess.RecipientFromTextFormat(signingKey) -// if err != nil { -// panic(err) -// } -// err = BinarySigningTrustStore.StoreSignet(rcpt) -// if err != nil { -// panic(err) -// } -// } -// } diff --git a/service/updates/helper/updates.go b/service/updates/helper/updates.go deleted file mode 100644 index 135c1222..00000000 --- a/service/updates/helper/updates.go +++ /dev/null @@ -1,95 +0,0 @@ -package helper - -// import ( -// "fmt" -// "runtime" - -// "github.com/tevino/abool" -// ) - -// const onWindows = runtime.GOOS == "windows" - -// var intelOnly = abool.New() - -// // IntelOnly specifies that only intel data is mandatory. -// func IntelOnly() { -// intelOnly.Set() -// } - -// // PlatformIdentifier converts identifier for the current platform. -// func PlatformIdentifier(identifier string) string { -// // From https://golang.org/pkg/runtime/#GOARCH -// // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. -// // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. -// return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) -// } - -// // MandatoryUpdates returns mandatory updates that should be loaded on install -// // or reset. -// func MandatoryUpdates() (identifiers []string) { -// // Intel -// identifiers = append( -// identifiers, - -// // Filter lists data -// "all/intel/lists/index.dsd", -// "all/intel/lists/base.dsdl", -// "all/intel/lists/intermediate.dsdl", -// "all/intel/lists/urgent.dsdl", - -// // Geo IP data -// "all/intel/geoip/geoipv4.mmdb.gz", -// "all/intel/geoip/geoipv6.mmdb.gz", -// ) - -// // Stop here if we only want intel data. -// if intelOnly.IsSet() { -// return identifiers -// } - -// // Binaries -// if onWindows { -// identifiers = append( -// identifiers, -// PlatformIdentifier("core/portmaster-core.exe"), -// PlatformIdentifier("kext/portmaster-kext.sys"), -// PlatformIdentifier("kext/portmaster-kext.pdb"), -// PlatformIdentifier("start/portmaster-start.exe"), -// PlatformIdentifier("notifier/portmaster-notifier.exe"), -// PlatformIdentifier("notifier/portmaster-wintoast.dll"), -// PlatformIdentifier("app2/portmaster-app.zip"), -// ) -// } else { -// identifiers = append( -// identifiers, -// PlatformIdentifier("core/portmaster-core"), -// PlatformIdentifier("start/portmaster-start"), -// PlatformIdentifier("notifier/portmaster-notifier"), -// PlatformIdentifier("app2/portmaster-app"), -// ) -// } - -// // Components, Assets and Data -// identifiers = append( -// identifiers, - -// // User interface components -// PlatformIdentifier("app/portmaster-app.zip"), -// "all/ui/modules/portmaster.zip", -// "all/ui/modules/assets.zip", -// ) - -// return identifiers -// } - -// // AutoUnpackUpdates returns assets that need unpacking. -// func AutoUnpackUpdates() []string { -// if intelOnly.IsSet() { -// return []string{} -// } - -// return []string{ -// PlatformIdentifier("app/portmaster-app.zip"), -// PlatformIdentifier("app2/portmaster-app.zip"), -// } -// } diff --git a/service/updates/index.go b/service/updates/index.go index b753450c..c9a5f29d 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -1,6 +1,7 @@ package updates import ( + "context" "fmt" "io" "net/http" @@ -18,13 +19,14 @@ type UpdateIndex struct { IndexURLs []string IndexFile string AutoApply bool + NeedsRestart bool } -func (ui *UpdateIndex) DownloadIndexFile(client *http.Client) (err error) { +func (ui *UpdateIndex) DownloadIndexFile(ctx context.Context, client *http.Client) (err error) { // Make sure dir exists _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) for _, url := range ui.IndexURLs { - err = ui.downloadIndexFileFromURL(client, url) + err = ui.downloadIndexFileFromURL(ctx, client, url) if err != nil { log.Warningf("updates: failed while downloading index file %s", err) continue @@ -36,9 +38,18 @@ func (ui *UpdateIndex) DownloadIndexFile(client *http.Client) (err error) { return } -func (ui *UpdateIndex) downloadIndexFileFromURL(client *http.Client, url string) error { +func (ui *UpdateIndex) downloadIndexFileFromURL(ctx context.Context, client *http.Client, url string) error { // Request the index file - resp, err := client.Get(url) + req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) + if err != nil { + return fmt.Errorf("failed to create GET request to %s: %w", url, err) + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + + // Perform request + resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed GET request to %s: %w", url, err) } diff --git a/service/updates/main.go b/service/updates/main.go index f9e68b8b..de15a98b 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -1,70 +1 @@ package updates - -import ( - "fmt" - "runtime" - "time" - - "github.com/safing/portmaster/base/database" -) - -const ( - onWindows = runtime.GOOS == "windows" - - enableSoftwareUpdatesKey = "core/automaticUpdates" - enableIntelUpdatesKey = "core/automaticIntelUpdates" - - // VersionUpdateEvent is emitted every time a new - // version of a monitored resource is selected. - // During module initialization VersionUpdateEvent - // is also emitted. - VersionUpdateEvent = "active version update" - - // ResourceUpdateEvent is emitted every time the - // updater successfully performed a resource update. - // ResourceUpdateEvent is emitted even if no new - // versions are available. Subscribers are expected - // to check if new versions of their resources are - // available by checking File.UpgradeAvailable(). - ResourceUpdateEvent = "resource update" -) - -var ( - userAgentFromFlag string - updateServerFromFlag string - - db = database.NewInterface(&database.Options{ - Local: true, - Internal: true, - }) - - // UserAgent is an HTTP User-Agent that is used to add - // more context to requests made by the registry when - // fetching resources from the update server. - UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) -) - -const ( - updateTaskRepeatDuration = 1 * time.Hour -) - -func stop() error { - // if registry != nil { - // err := registry.Cleanup() - // if err != nil { - // log.Warningf("updates: failed to clean up registry: %s", err) - // } - // } - - return nil -} - -// RootPath returns the root path used for storing updates. -func RootPath() string { - // if !module.Online() { - // return "" - // } - - // return registry.StorageDir().Path - return "" -} diff --git a/service/updates/module.go b/service/updates/module.go index 99c110da..5caf2ab4 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -1,10 +1,12 @@ package updates import ( + "context" "errors" "fmt" "net/http" "os" + "runtime" "time" semver "github.com/hashicorp/go-version" @@ -15,7 +17,29 @@ import ( "github.com/safing/portmaster/service/mgr" ) -const updateAvailableNotificationID = "updates:update-available" +const ( + updateTaskRepeatDuration = 1 * time.Hour + updateAvailableNotificationID = "updates:update-available" + + // VersionUpdateEvent is emitted every time a new + // version of a monitored resource is selected. + // During module initialization VersionUpdateEvent + // is also emitted. + VersionUpdateEvent = "active version update" + + // ResourceUpdateEvent is emitted every time the + // updater successfully performed a resource update. + // ResourceUpdateEvent is emitted even if no new + // versions are available. Subscribers are expected + // to check if new versions of their resources are + // available by checking File.UpgradeAvailable(). + ResourceUpdateEvent = "resource update" +) + +// UserAgent is an HTTP User-Agent that is used to add +// more context to requests made by the registry when +// fetching resources from the update server. +var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) type File struct { id string @@ -75,7 +99,7 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { // Events module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil) - module.updateCheckWorkerMgr.Repeat(1 * time.Hour) + module.updateCheckWorkerMgr.Repeat(updateTaskRepeatDuration) module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", module.applyUpdates, nil) var err error @@ -86,44 +110,10 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { // Add bundle artifacts to registry. module.processBundle(module.bundle) - err = module.registerEndpoints() - if err != nil { - log.Errorf("failed to register endpoints: %s", err) - } return module, nil } -func (u *Updates) registerEndpoints() error { - if err := api.RegisterEndpoint(api.Endpoint{ - Name: "Check for update", - Description: "Trigger update check", - Path: "updates/check", - Read: api.PermitAnyone, - ActionFunc: func(ar *api.Request) (msg string, err error) { - u.updateCheckWorkerMgr.Go() - return "Check for updates triggered", nil - }, - }); err != nil { - return err - } - - if err := api.RegisterEndpoint(api.Endpoint{ - Name: "Apply update", - Description: "Triggers update", - Path: "updates/apply", - Read: api.PermitAnyone, - ActionFunc: func(ar *api.Request) (msg string, err error) { - u.upgraderWorkerMgr.Go() - return "Apply updates triggered", nil - }, - }); err != nil { - return err - } - - return nil -} - func (reg *Updates) processBundle(bundle *Bundle) { for _, artifact := range bundle.Artifacts { artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) @@ -131,9 +121,9 @@ func (reg *Updates) processBundle(bundle *Bundle) { } } -func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { +func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { httpClient := http.Client{} - err := u.updateIndex.DownloadIndexFile(&httpClient) + err := u.updateIndex.DownloadIndexFile(wc.Ctx(), &httpClient) if err != nil { return fmt.Errorf("failed to download index file: %s", err) } @@ -155,16 +145,23 @@ func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { } log.Infof("updates: check complete: downloading new version: %s %s", u.updateBundle.Name, u.updateBundle.Version) - err = u.downloadUpdates(&httpClient) + err = u.downloadUpdates(wc.Ctx(), &httpClient) if err != nil { log.Errorf("updates: failed to download bundle: %s", err) } else { - notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{ - ID: "apply", - Text: "Apply", - Type: notifications.ActionTypeInjectEvent, - Payload: "apply-updates", - }) + if u.updateIndex.AutoApply { + u.upgraderWorkerMgr.Go() + } else { + notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{ + ID: "apply", + Text: "Apply", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, + }) + } } return nil } @@ -183,7 +180,7 @@ func (u *Updates) checkVersionIncrement() (bool, error) { return downloadVersion.GreaterThan(currentVersion), nil } -func (u *Updates) downloadUpdates(client *http.Client) error { +func (u *Updates) downloadUpdates(ctx context.Context, client *http.Client) error { if u.updateBundle == nil { // checkForUpdates needs to be called before this. return fmt.Errorf("no valid update bundle found") @@ -193,7 +190,7 @@ func (u *Updates) downloadUpdates(client *http.Client) error { if err != nil { log.Warningf("updates: error while coping file from current to update: %s", err) } - u.updateBundle.DownloadAndVerify(client, u.updateIndex.DownloadDirectory) + u.updateBundle.DownloadAndVerify(ctx, client, u.updateIndex.DownloadDirectory) return nil } @@ -208,18 +205,23 @@ func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error { return fmt.Errorf("there is no new version to apply") } + // Verify files of the downloaded files. err = u.updateBundle.Verify(u.updateIndex.DownloadDirectory) if err != nil { - return fmt.Errorf("failed to apply update: %s", err) + return fmt.Errorf("failed to verify downloaded files: %s", err) } + // New version is downloaded and verified. Start the update process + log.Infof("update: starting update: %s %s -> %s", u.bundle.Name, u.bundle.Version, u.updateBundle.Version) err = switchFolders(u.updateIndex, *u.updateBundle) if err != nil { // TODO(vladimir): Send notification to UI log.Errorf("updates: failed to apply updates: %s", err) } else { // TODO(vladimir): Prompt user to restart? - u.instance.Restart() + if u.updateIndex.NeedsRestart { + u.instance.Restart() + } } return nil } @@ -274,7 +276,7 @@ func (u *Updates) GetFile(id string) (*File, error) { // Stop stops the module. func (u *Updates) Stop() error { - return stop() + return nil } type instance interface { diff --git a/service/updates/notify.go b/service/updates/notify.go deleted file mode 100644 index 1a539f7a..00000000 --- a/service/updates/notify.go +++ /dev/null @@ -1,176 +0,0 @@ -package updates - -import ( - "sync/atomic" - "time" -) - -const ( - updateFailed = "updates:failed" - updateSuccess = "updates:success" - updateSuccessPending = "updates:success-pending" - updateSuccessDownloaded = "updates:success-downloaded" - - failedUpdateNotifyDurationThreshold = 24 * time.Hour - failedUpdateNotifyCountThreshold = 3 -) - -var updateFailedCnt = new(atomic.Int32) - -func (u *Updates) notificationsEnabled() bool { - return u.instance.Notifications() != nil -} - -// func notifyUpdateSuccess(force bool) { -// if !module.notificationsEnabled() { -// return -// } - -// updateFailedCnt.Store(0) -// module.states.Clear() -// updateState := registry.GetState().Updates - -// flavor := updateSuccess -// switch { -// case len(updateState.PendingDownload) > 0: -// // Show notification if there are pending downloads. -// flavor = updateSuccessPending -// case updateState.LastDownloadAt != nil && -// time.Since(*updateState.LastDownloadAt) < 5*time.Second: -// // Show notification if we downloaded something within the last minute. -// flavor = updateSuccessDownloaded -// case force: -// // Always show notification if update was manually triggered. -// default: -// // Otherwise, the update was uneventful. Do not show notification. -// return -// } - -// switch flavor { -// case updateSuccess: -// notifications.Notify(¬ifications.Notification{ -// EventID: updateSuccess, -// Type: notifications.Info, -// Title: "Portmaster Is Up-To-Date", -// Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), -// Expires: time.Now().Add(1 * time.Minute).Unix(), -// AvailableActions: []*notifications.Action{ -// { -// ID: "ack", -// Text: "OK", -// }, -// }, -// }) - -// case updateSuccessPending: -// msg := fmt.Sprintf( -// `%d updates are available for download: - -// - %s - -// Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, -// len(updateState.PendingDownload), -// strings.Join(updateState.PendingDownload, "\n- "), -// ) - -// notifications.Notify(¬ifications.Notification{ -// EventID: updateSuccess, -// Type: notifications.Info, -// Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), -// Message: msg, -// AvailableActions: []*notifications.Action{ -// { -// ID: "ack", -// Text: "OK", -// }, -// { -// ID: "download", -// Text: "Download Now", -// Type: notifications.ActionTypeWebhook, -// Payload: ¬ifications.ActionTypeWebhookPayload{ -// URL: apiPathCheckForUpdates + "?download", -// ResultAction: "display", -// }, -// }, -// }, -// }) - -// case updateSuccessDownloaded: -// msg := fmt.Sprintf( -// `%d updates were downloaded and applied: - -// - %s - -// %s -// `, -// len(updateState.LastDownload), -// strings.Join(updateState.LastDownload, "\n- "), -// getUpdatingInfoMsg(), -// ) - -// notifications.Notify(¬ifications.Notification{ -// EventID: updateSuccess, -// Type: notifications.Info, -// Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), -// Message: msg, -// Expires: time.Now().Add(1 * time.Minute).Unix(), -// AvailableActions: []*notifications.Action{ -// { -// ID: "ack", -// Text: "OK", -// }, -// }, -// }) - -// } -// } - -func getUpdatingInfoMsg() string { - switch { - case enableSoftwareUpdates() && enableIntelUpdates(): - return "You will be notified of important updates that need restarting." - case enableIntelUpdates(): - return "Automatic software updates are disabled, but you will be notified when a new software update is ready to be downloaded and applied." - default: - return "Automatic software updates are disabled. Please check for updates regularly yourself." - } -} - -// func notifyUpdateCheckFailed(force bool, err error) { -// if !module.notificationsEnabled() { -// return -// } - -// failedCnt := updateFailedCnt.Add(1) -// lastSuccess := registry.GetState().Updates.LastSuccessAt - -// switch { -// case force: -// // Always show notification if update was manually triggered. -// case failedCnt < failedUpdateNotifyCountThreshold: -// // Not failed often enough for notification. -// return -// case lastSuccess == nil: -// // No recorded successful update. -// case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess): -// // Failed too recently for notification. -// return -// } - -// notifications.NotifyWarn( -// updateFailed, -// "Update Check Failed", -// fmt.Sprintf( -// "Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s", -// err, -// ), -// notifications.Action{ -// Text: "Try Again Now", -// Type: notifications.ActionTypeWebhook, -// Payload: ¬ifications.ActionTypeWebhookPayload{ -// URL: apiPathCheckForUpdates, -// ResultAction: "display", -// }, -// }, -// ).SyncWithState(module.states) -// } diff --git a/service/updates/os_integration_default.go b/service/updates/os_integration_default.go deleted file mode 100644 index b817c351..00000000 --- a/service/updates/os_integration_default.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !linux -// +build !linux - -package updates - -func upgradeSystemIntegration() error { - return nil -} diff --git a/service/updates/os_integration_linux.go b/service/updates/os_integration_linux.go deleted file mode 100644 index cd1b6137..00000000 --- a/service/updates/os_integration_linux.go +++ /dev/null @@ -1,201 +0,0 @@ -package updates - -// import ( -// "crypto/sha256" -// _ "embed" -// "encoding/hex" -// "errors" -// "fmt" -// "io/fs" -// "os" -// "path/filepath" - -// "github.com/tevino/abool" -// "golang.org/x/exp/slices" - -// "github.com/safing/portmaster/base/dataroot" -// "github.com/safing/portmaster/base/log" -// ) - -// var ( -// portmasterCoreServiceFilePath = "portmaster.service" -// portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" -// backupExtension = ".backup" - -// //go:embed assets/portmaster.service -// currentPortmasterCoreServiceFile []byte - -// checkedSystemIntegration = abool.New() - -// // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. -// ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") -// ) - -// func upgradeSystemIntegration() { -// // Check if we already checked the system integration. -// if !checkedSystemIntegration.SetToIf(false, true) { -// return -// } - -// // Upgrade portmaster core systemd service. -// err := upgradeSystemIntegrationFile( -// "portmaster core systemd service", -// filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), -// 0o0600, -// currentPortmasterCoreServiceFile, -// []string{ -// "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 -// "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 -// "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 -// }, -// ) -// if err != nil { -// log.Warningf("updates: %s", err) -// return -// } - -// // Upgrade portmaster notifier systemd user service. -// // Permissions only! -// err = upgradeSystemIntegrationFile( -// "portmaster notifier systemd user service", -// filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), -// 0o0644, -// nil, // Do not update contents. -// nil, // Do not update contents. -// ) -// if err != nil { -// log.Warningf("updates: %s", err) -// return -// } -// } - -// // upgradeSystemIntegrationFile upgrades the file contents and permissions. -// // System integration files are not necessarily present and may also be -// // edited by third parties, such as the OS itself or other installers. -// // The supplied hashes must be sha256 hex-encoded. -// func upgradeSystemIntegrationFile( -// name string, -// filePath string, -// fileMode fs.FileMode, -// fileData []byte, -// permittedUpgradeHashes []string, -// ) error { -// // Upgrade file contents. -// if len(fileData) > 0 { -// if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { -// return err -// } -// } - -// // Upgrade file permissions. -// if fileMode != 0 { -// if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { -// return err -// } -// } - -// return nil -// } - -// // upgradeSystemIntegrationFileContents upgrades the file contents. -// // System integration files are not necessarily present and may also be -// // edited by third parties, such as the OS itself or other installers. -// // The supplied hashes must be sha256 hex-encoded. -// func upgradeSystemIntegrationFileContents( -// name string, -// filePath string, -// fileData []byte, -// permittedUpgradeHashes []string, -// ) error { -// // Read existing file. -// existingFileData, err := os.ReadFile(filePath) -// if err != nil { -// if errors.Is(err, os.ErrNotExist) { -// return nil -// } -// return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) -// } - -// // Check if file is already the current version. -// existingSum := sha256.Sum256(existingFileData) -// existingHexSum := hex.EncodeToString(existingSum[:]) -// currentSum := sha256.Sum256(fileData) -// currentHexSum := hex.EncodeToString(currentSum[:]) -// if existingHexSum == currentHexSum { -// log.Debugf("updates: %s at %s is up to date", name, filePath) -// return nil -// } - -// // Check if we are allowed to upgrade from the existing file. -// if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { -// return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) -// } - -// // Start with upgrade! - -// // Make backup of existing file. -// err = CopyFile(filePath, filePath+backupExtension) -// if err != nil { -// return fmt.Errorf( -// "failed to create backup of %s from %s to %s: %w", -// name, -// filePath, -// filePath+backupExtension, -// err, -// ) -// } - -// // Open destination file for writing. -// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) -// // if err != nil { -// // return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) -// // } -// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - -// // // Write file. -// // _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) -// // if err != nil { -// // return err -// // } - -// // // Finalize file. -// // err = atomicDstFile.CloseAtomicallyReplace() -// // if err != nil { -// // return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) -// // } - -// log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) -// return nil -// } - -// // upgradeSystemIntegrationFilePermissions upgrades the file permissions. -// // System integration files are not necessarily present and may also be -// // edited by third parties, such as the OS itself or other installers. -// func upgradeSystemIntegrationFilePermissions( -// name string, -// filePath string, -// fileMode fs.FileMode, -// ) error { -// // Get current file permissions. -// stat, err := os.Stat(filePath) -// if err != nil { -// if errors.Is(err, os.ErrNotExist) { -// return nil -// } -// return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) -// } - -// // If permissions are as expected, do nothing. -// if stat.Mode().Perm() == fileMode { -// return nil -// } - -// // Otherwise, set correct permissions. -// err = os.Chmod(filePath, fileMode) -// if err != nil { -// return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) -// } - -// log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) -// return nil -// } diff --git a/service/updates/state.go b/service/updates/state.go deleted file mode 100644 index ef104308..00000000 --- a/service/updates/state.go +++ /dev/null @@ -1,49 +0,0 @@ -package updates - -// import ( -// "github.com/safing/portmaster/base/database/record" -// "github.com/safing/portmaster/base/runtime" -// "github.com/safing/portmaster/base/updater" -// ) - -// var pushRegistryStatusUpdate runtime.PushFunc - -// // RegistryStateExport is a wrapper to export the registry state. -// type RegistryStateExport struct { -// record.Base -// *updater.RegistryState -// } - -// func exportRegistryState(s *updater.RegistryState) *RegistryStateExport { -// // if s == nil { -// // state := registry.GetState() -// // s = &state -// // } - -// export := &RegistryStateExport{ -// RegistryState: s, -// } - -// export.CreateMeta() -// export.SetKey("runtime:core/updates/state") - -// return export -// } - -// func pushRegistryState(s *updater.RegistryState) { -// export := exportRegistryState(s) -// pushRegistryStatusUpdate(export) -// } - -// func registerRegistryStateProvider() (err error) { -// registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { -// return []record.Record{exportRegistryState(nil)}, nil -// }) - -// pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider) -// if err != nil { -// return err -// } - -// return nil -// } diff --git a/service/updates/updater.go b/service/updates/updater.go index 4a0358ec..f76ed2b9 100644 --- a/service/updates/updater.go +++ b/service/updates/updater.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "github.com/safing/portmaster/base/log" @@ -29,6 +30,7 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { } // Move current version files into purge folder. + log.Debugf("updates: removing the old version") for _, file := range files { currentFilepath := filepath.Join(updateIndex.Directory, file.Name()) purgePath := filepath.Join(updateIndex.PurgeDirectory, file.Name()) @@ -39,6 +41,7 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { } // Move the new index file + log.Debugf("updates: installing the new version") indexFile := filepath.Join(updateIndex.DownloadDirectory, updateIndex.IndexFile) newIndexFile := filepath.Join(updateIndex.Directory, updateIndex.IndexFile) err = os.Rename(indexFile, newIndexFile) @@ -52,9 +55,27 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { toFilepath := filepath.Join(updateIndex.Directory, artifact.Filename) err = os.Rename(fromFilepath, toFilepath) if err != nil { - return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) + log.Errorf("failed to move file %s: %s", fromFilepath, err) + } else { + log.Debugf("updates: %s moved", artifact.Filename) + } + + // Special case for linux. + // When installed the portmaster ui path is `/usr/bin/portmaster`. During update the ui will be placed in `/usr/lib/portmaster/portmaster` + // After an update the original binary should be deleted and replaced by symlink + // `/usr/bin/portmaster` -> `/usr/lib/portmaster/portmaster` + if runtime.GOOS == "linux" && artifact.Filename == "portmaster" && artifact.Platform == currentPlatform { + err = makeSymlinkForUI(updateIndex.Directory) + if err != nil { + log.Errorf("failed to create symlink for the ui: %s", err) + } else { + log.Infof("ui symlink successfully created") + } } } + + log.Debugf("updates: update complete") + return nil } @@ -76,3 +97,11 @@ func deleteUnfinishedDownloads(rootDir string) error { } return nil } + +func makeSymlinkForUI(directory string) error { + err := os.Symlink(filepath.Join(directory, "portmaster"), "/usr/bin/portmaster") + if err != nil { + return fmt.Errorf("failed to create symlink: %w", err) + } + return nil +} diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go deleted file mode 100644 index 334ebe88..00000000 --- a/service/updates/upgrader.go +++ /dev/null @@ -1,403 +0,0 @@ -package updates - -// import ( -// "context" -// "fmt" -// "os" -// "os/exec" -// "path/filepath" -// "regexp" -// "strings" -// "time" - -// processInfo "github.com/shirou/gopsutil/process" -// "github.com/tevino/abool" - -// "github.com/safing/portmaster/base/dataroot" -// "github.com/safing/portmaster/base/info" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/notifications" -// "github.com/safing/portmaster/base/rng" -// "github.com/safing/portmaster/base/updater" -// "github.com/safing/portmaster/service/mgr" -// ) - -// const ( -// upgradedSuffix = "-upgraded" -// exeExt = ".exe" -// ) - -// var ( -// upgraderActive = abool.NewBool(false) - -// pmCtrlUpdate *updater.File -// pmCoreUpdate *updater.File - -// spnHubUpdate *updater.File - -// rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) -// ) - -// func initUpgrader() error { -// // module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) -// return nil -// } - -// func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { -// // Lock runs, but discard additional runs. -// if !upgraderActive.SetToIf(false, true) { -// return false, nil -// } -// defer upgraderActive.SetTo(false) - -// // Upgrade portmaster-start. -// err = upgradePortmasterStart() -// if err != nil { -// log.Warningf("updates: failed to upgrade portmaster-start: %s", err) -// } - -// // Upgrade based on binary. -// binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] -// switch binBaseName { -// case "portmaster-core": -// // Notify about upgrade. -// if err := upgradeCoreNotify(); err != nil { -// log.Warningf("updates: failed to notify about core upgrade: %s", err) -// } - -// // Fix chrome sandbox permissions. -// // if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { -// // log.Warningf("updates: failed to handle electron upgrade: %s", err) -// // } - -// // Upgrade system integration. -// upgradeSystemIntegration() - -// case "spn-hub": -// // Trigger upgrade procedure. -// if err := upgradeHub(); err != nil { -// log.Warningf("updates: failed to initiate hub upgrade: %s", err) -// } -// } - -// return false, nil -// } - -// func upgradeCoreNotify() error { -// if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { -// return nil -// } - -// // make identifier -// identifier := "core/portmaster-core" // identifier, use forward slash! -// if onWindows { -// identifier += exeExt -// } - -// // get newest portmaster-core -// // newFile, err := GetPlatformFile(identifier) -// // if err != nil { -// // return err -// // } -// // pmCoreUpdate = newFile - -// // check for new version -// if info.VersionNumber() != pmCoreUpdate.Version() { -// n := notifications.Notify(¬ifications.Notification{ -// EventID: "updates:core-update-available", -// Type: notifications.Info, -// Title: fmt.Sprintf( -// "Portmaster Update v%s Is Ready!", -// pmCoreUpdate.Version(), -// ), -// Category: "Core", -// Message: fmt.Sprintf( -// `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, -// pmCoreUpdate.Version(), -// ), -// ShowOnSystem: true, -// AvailableActions: []*notifications.Action{ -// // TODO: Use special UI action in order to reload UI on restart. -// { -// ID: "restart", -// Text: "Restart", -// }, -// { -// ID: "later", -// Text: "Not now", -// }, -// }, -// }) -// n.SetActionFunction(upgradeCoreNotifyActionHandler) - -// log.Debugf("updates: new portmaster version available, sending notification to user") -// } - -// return nil -// } - -// func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { -// switch n.SelectedActionID { -// case "restart": -// log.Infof("updates: user triggered restart via core update notification") -// RestartNow() -// case "later": -// n.Delete() -// } - -// return nil -// } - -// func upgradeHub() error { -// if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { -// return nil -// } - -// // Make identifier for getting file from updater. -// identifier := "hub/spn-hub" // identifier, use forward slash! -// if onWindows { -// identifier += exeExt -// } - -// // Get newest spn-hub file. -// // newFile, err := GetPlatformFile(identifier) -// // if err != nil { -// // return err -// // } -// // spnHubUpdate = newFile - -// // Check if the new version is different. -// if info.GetInfo().Version != spnHubUpdate.Version() { -// // Get random delay with up to three hours. -// delayMinutes, err := rng.Number(3 * 60) -// if err != nil { -// return err -// } - -// // Delay restart for at least one hour for preparations. -// DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) - -// // Increase update checks in order to detect aborts better. -// // if !disableTaskSchedule { -// // module.updateBinaryWorkerMgr.Repeat(10 * time.Minute) -// // } -// } else { -// AbortRestart() - -// // Set update task schedule back to normal. -// // if !disableTaskSchedule { -// // module.updateBinaryWorkerMgr.Repeat(updateTaskRepeatDuration) -// // } -// } - -// return nil -// } - -// func upgradePortmasterStart() error { -// filename := "portmaster-start" -// if onWindows { -// filename += exeExt -// } - -// // check if we can upgrade -// if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { -// // get newest portmaster-start -// // newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! -// // if err != nil { -// // return err -// // } -// // pmCtrlUpdate = newFile -// } else { -// return nil -// } - -// // update portmaster-start in data root -// rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) -// err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) -// if err != nil { -// return err -// } - -// return nil -// } - -// func warnOnIncorrectParentPath() { -// expectedFileName := "portmaster-start" -// if onWindows { -// expectedFileName += exeExt -// } - -// // upgrade parent process, if it's portmaster-start -// parent, err := processInfo.NewProcess(int32(os.Getppid())) -// if err != nil { -// log.Tracef("could not get parent process: %s", err) -// return -// } -// parentName, err := parent.Name() -// if err != nil { -// log.Tracef("could not get parent process name: %s", err) -// return -// } -// if parentName != expectedFileName { -// // Only warn about this if not in dev mode. -// if !devMode() { -// log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) -// } - -// // TODO(ppacher): once we released a new installer and folks had time -// // to update we should send a module warning/hint to the -// // UI notifying the user that he's still using portmaster-control. -// return -// } - -// // parentPath, err := parent.Exe() -// // if err != nil { -// // log.Tracef("could not get parent process path: %s", err) -// // return -// // } - -// // absPath, err := filepath.Abs(parentPath) -// // if err != nil { -// // log.Tracef("could not get absolut parent process path: %s", err) -// // return -// // } - -// // root := filepath.Dir(registry.StorageDir().Path) -// // if !strings.HasPrefix(absPath, root) { -// // log.Warningf("detected unexpected path %s for portmaster-start", absPath) -// // notifications.NotifyWarn( -// // "updates:unsupported-parent", -// // "Unsupported Launcher", -// // fmt.Sprintf( -// // "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", -// // expectedFileName, -// // absPath, -// // filepath.Join(root, expectedFileName), -// // ), -// // ) -// // } -// } - -// func upgradeBinary(fileToUpgrade string, file *updater.File) error { -// fileExists := false -// _, err := os.Stat(fileToUpgrade) -// if err == nil { -// // file exists and is accessible -// fileExists = true -// } - -// if fileExists { -// // get current version -// var currentVersion string -// cmd := exec.Command(fileToUpgrade, "version", "--short") -// out, err := cmd.Output() -// if err == nil { -// // abort if version matches -// currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") -// if currentVersion == file.Version() { -// log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) -// // already up to date! -// return nil -// } -// } else { -// log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) -// currentVersion = "0.0.0" -// } - -// // test currentVersion for sanity -// if !rawVersionRegex.MatchString(currentVersion) { -// log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) -// } - -// // try removing old version -// err = os.Remove(fileToUpgrade) -// if err != nil { -// // ensure tmp dir is here -// // err = registry.TmpDir().Ensure() -// // if err != nil { -// // return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) -// // } - -// // maybe we're on windows and it's in use, try moving -// // err = os.Rename(fileToUpgrade, filepath.Join( -// // registry.TmpDir().Path, -// // fmt.Sprintf( -// // "%s-%d%s", -// // filepath.Base(fileToUpgrade), -// // time.Now().UTC().Unix(), -// // upgradedSuffix, -// // ), -// // )) -// // if err != nil { -// // return fmt.Errorf("unable to move file that needs upgrade: %w", err) -// // } -// } -// } - -// // copy upgrade -// err = CopyFile(file.Path(), fileToUpgrade) -// if err != nil { -// // try again -// time.Sleep(1 * time.Second) -// err = CopyFile(file.Path(), fileToUpgrade) -// if err != nil { -// return err -// } -// } - -// // check permissions -// if !onWindows { -// info, err := os.Stat(fileToUpgrade) -// if err != nil { -// return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) -// } -// if info.Mode() != 0o0755 { -// err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. -// if err != nil { -// return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) -// } -// } -// } - -// log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) -// return nil -// } - -// // CopyFile atomically copies a file using the update registry's tmp dir. -// func CopyFile(srcPath, dstPath string) error { -// // check tmp dir -// // err := registry.TmpDir().Ensure() -// // if err != nil { -// // return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) -// // } - -// // open file for writing -// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) -// // if err != nil { -// // return fmt.Errorf("could not create temp file for atomic copy: %w", err) -// // } -// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - -// // // open source -// // srcFile, err := os.Open(srcPath) -// // if err != nil { -// // return err -// // } -// // defer func() { -// // _ = srcFile.Close() -// // }() - -// // // copy data -// // _, err = io.Copy(atomicDstFile, srcFile) -// // if err != nil { -// // return err -// // } - -// // // finalize file -// // err = atomicDstFile.CloseAtomicallyReplace() -// // if err != nil { -// // return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) -// // } - -// return nil -// } From 89b533f949ffb9e9f2bdc4da8f804420ee142377 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 20 Sep 2024 17:36:07 +0300 Subject: [PATCH 15/62] [WIP] Refactoring --- service/intel/filterlists/module.go | 2 +- service/updates/bundle.go | 232 ++----------------- service/updates/downloader.go | 343 ++++++++++++++++++++++++++++ service/updates/index.go | 76 ------ service/updates/module.go | 175 ++++---------- service/updates/registry.go | 177 ++++++++++++++ service/updates/updater.go | 107 --------- 7 files changed, 587 insertions(+), 525 deletions(-) create mode 100644 service/updates/downloader.go create mode 100644 service/updates/registry.go delete mode 100644 service/updates/updater.go diff --git a/service/intel/filterlists/module.go b/service/intel/filterlists/module.go index ecded5dc..8f3a5a5e 100644 --- a/service/intel/filterlists/module.go +++ b/service/intel/filterlists/module.go @@ -62,7 +62,7 @@ func prep() error { if ignoreUpdateEvents.IsSet() { return false, nil } - log.Debugf("performing filter list upadte") + log.Debugf("performing filter list update") return false, tryListUpdate(wc.Ctx()) }) diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 904ae8fd..02b6089c 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -1,23 +1,16 @@ package updates import ( - "archive/zip" "bytes" - "compress/gzip" - "context" "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "io" - "net/http" "os" "path/filepath" "runtime" "time" - - "github.com/safing/portmaster/base/log" ) const MaxUnpackSize = 1 << 30 // 2^30 == 1GB @@ -33,6 +26,19 @@ type Artifact struct { Version string `json:"Version,omitempty"` } +func (a *Artifact) GetFileMode() os.FileMode { + // Special case for portmaster ui. Should be able to be executed from the regular user + if a.Platform == currentPlatform && a.Filename == "portmaster" { + return executableUIFileMode + } + + if a.Platform == currentPlatform { + return executableFileMode + } + + return defaultFileMode +} + type Bundle struct { Name string `json:"Bundle"` Version string `json:"Version"` @@ -40,10 +46,9 @@ type Bundle struct { Artifacts []Artifact `json:"Artifacts"` } -func ParseBundle(dir string, indexFile string) (*Bundle, error) { - filepath := fmt.Sprintf("%s/%s", dir, indexFile) +func ParseBundle(indexFile string) (*Bundle, error) { // Check if the file exists. - file, err := os.Open(filepath) + file, err := os.Open(indexFile) if err != nil { return nil, fmt.Errorf("failed to open index file: %w", err) } @@ -59,7 +64,7 @@ func ParseBundle(dir string, indexFile string) (*Bundle, error) { var bundle Bundle err = json.Unmarshal(content, &bundle) if err != nil { - return nil, fmt.Errorf("%s %w", filepath, err) + return nil, fmt.Errorf("%s %w", indexFile, err) } // Filter artifacts @@ -74,70 +79,6 @@ func ParseBundle(dir string, indexFile string) (*Bundle, error) { return &bundle, nil } -// CopyMatchingFilesFromCurrent check if there the current bundle files has matching files with the new bundle and copies them if they match. -func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, newDir string) error { - // Make sure new dir exists - _ = os.MkdirAll(newDir, defaultDirMode) - - for _, currentArtifact := range current.Artifacts { - new: - for _, newArtifact := range bundle.Artifacts { - if currentArtifact.Filename == newArtifact.Filename { - if currentArtifact.SHA256 == newArtifact.SHA256 { - // Read the content of the current file. - sourceFilePath := filepath.Join(currentDir, newArtifact.Filename) - content, err := os.ReadFile(sourceFilePath) - if err != nil { - return fmt.Errorf("failed to read file %s: %w", sourceFilePath, err) - } - - // Check if the content matches the artifact hash - expectedHash, err := hex.DecodeString(newArtifact.SHA256) - if err != nil || len(expectedHash) != sha256.Size { - return fmt.Errorf("invalid artifact hash %s: %w", newArtifact.SHA256, err) - } - hash := sha256.Sum256(content) - if !bytes.Equal(expectedHash, hash[:]) { - return fmt.Errorf("expected and file hash mismatch: %s", sourceFilePath) - } - - // Create new file - destFilePath := filepath.Join(newDir, newArtifact.Filename) - err = os.WriteFile(destFilePath, content, defaultFileMode) - if err != nil { - return fmt.Errorf("failed to write to file %s: %w", destFilePath, err) - } - log.Debugf("updates: file copied from current version: %s", newArtifact.Filename) - } - break new - } - } - } - return nil -} - -func (bundle Bundle) DownloadAndVerify(ctx context.Context, client *http.Client, dir string) { - // Make sure dir exists - _ = os.MkdirAll(dir, defaultDirMode) - - for _, artifact := range bundle.Artifacts { - filePath := filepath.Join(dir, artifact.Filename) - - // Check file is already downloaded and valid. - exists, _ := checkIfFileIsValid(filePath, artifact) - if exists { - log.Debugf("updates: file already downloaded: %s", filePath) - continue - } - - // Download artifact - err := processArtifact(ctx, client, artifact, filePath) - if err != nil { - log.Errorf("updates: %s", err) - } - } -} - // Verify checks if the files are present int the dataDir and have the correct hash. func (bundle Bundle) Verify(dir string) error { for _, artifact := range bundle.Artifacts { @@ -179,144 +120,3 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } return true, nil } - -func processArtifact(ctx context.Context, client *http.Client, artifact Artifact, filePath string) error { - providedHash, err := hex.DecodeString(artifact.SHA256) - if err != nil || len(providedHash) != sha256.Size { - return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) - } - - // Download - log.Debugf("updates: downloading file: %s", artifact.Filename) - content, err := downloadFile(ctx, client, artifact.URLs) - if err != nil { - return fmt.Errorf("failed to download artifact: %w", err) - } - - // Decompress - if artifact.Unpack != "" { - content, err = unpack(artifact.Unpack, content) - if err != nil { - return fmt.Errorf("failed to decompress artifact: %w", err) - } - } - - // Verify - hash := sha256.Sum256(content) - if !bytes.Equal(providedHash, hash[:]) { - return fmt.Errorf("failed to verify artifact: %s", artifact.Filename) - } - - // Save - tmpFilename := fmt.Sprintf("%s.download", filePath) - fileMode := defaultFileMode - if artifact.Platform == currentPlatform { - fileMode = executableFileMode - } - err = os.WriteFile(tmpFilename, content, fileMode) - if err != nil { - return fmt.Errorf("failed to write to file: %w", err) - } - - // Rename - err = os.Rename(tmpFilename, filePath) - if err != nil { - return fmt.Errorf("failed to rename file: %w", err) - } - - log.Infof("updates: file downloaded and verified: %s", artifact.Filename) - - return nil -} - -func downloadFile(ctx context.Context, client *http.Client, urls []string) ([]byte, error) { - for _, url := range urls { - // Try to make the request - req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) - if err != nil { - log.Warningf("failed to create GET request to %s: %s", url, err) - continue - } - if UserAgent != "" { - req.Header.Set("User-Agent", UserAgent) - } - resp, err := client.Do(req) - if err != nil { - log.Warningf("failed a get file request to: %s", err) - continue - } - defer func() { _ = resp.Body.Close() }() - - // Check if the server returned an error - if resp.StatusCode != http.StatusOK { - log.Warningf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) - continue - } - - content, err := io.ReadAll(resp.Body) - if err != nil { - log.Warningf("failed to read body of response: %s", err) - continue - } - return content, nil - } - - return nil, fmt.Errorf("failed to download file from the provided urls") -} - -func unpack(cType string, fileBytes []byte) ([]byte, error) { - switch cType { - case "zip": - return decompressZip(fileBytes) - case "gz": - return decompressGzip(fileBytes) - default: - return nil, fmt.Errorf("unsupported compression type") - } -} - -func decompressGzip(data []byte) ([]byte, error) { - // Create a gzip reader from the byte array - gzipReader, err := gzip.NewReader(bytes.NewReader(data)) - if err != nil { - return nil, fmt.Errorf("failed to create gzip reader: %w", err) - } - defer func() { _ = gzipReader.Close() }() - - var buf bytes.Buffer - _, err = io.CopyN(&buf, gzipReader, MaxUnpackSize) - if err != nil && !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("failed to read gzip file: %w", err) - } - - return buf.Bytes(), nil -} - -func decompressZip(data []byte) ([]byte, error) { - // Create a zip reader from the byte array - zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - return nil, fmt.Errorf("failed to create zip reader: %w", err) - } - - // Ensure there is only one file in the zip - if len(zipReader.File) != 1 { - return nil, fmt.Errorf("zip file must contain exactly one file") - } - - // Read the single file in the zip - file := zipReader.File[0] - fileReader, err := file.Open() - if err != nil { - return nil, fmt.Errorf("failed to open file in zip: %w", err) - } - defer func() { _ = fileReader.Close() }() - - var buf bytes.Buffer - _, err = io.CopyN(&buf, fileReader, MaxUnpackSize) - if err != nil && !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("failed to read file in zip: %w", err) - } - - return buf.Bytes(), nil -} diff --git a/service/updates/downloader.go b/service/updates/downloader.go new file mode 100644 index 00000000..3cc35624 --- /dev/null +++ b/service/updates/downloader.go @@ -0,0 +1,343 @@ +package updates + +import ( + "archive/zip" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + semver "github.com/hashicorp/go-version" + "github.com/safing/portmaster/base/log" +) + +type Downloader struct { + dir string + indexFile string + indexURLs []string + bundle *Bundle + version *semver.Version + + httpClient http.Client +} + +func CreateDownloader(index UpdateIndex) Downloader { + return Downloader{ + dir: index.DownloadDirectory, + indexFile: index.IndexFile, + indexURLs: index.IndexURLs, + } +} + +func (d *Downloader) downloadIndexFile(ctx context.Context) (err error) { + // Make sure dir exists + _ = os.MkdirAll(d.dir, defaultDirMode) + for _, url := range d.indexURLs { + err = d.downloadIndexFileFromURL(ctx, url) + if err != nil { + log.Warningf("updates: failed while downloading index file %s", err) + continue + } + // Downloading was successful. + err = nil + break + } + + if err == nil { + err = d.parseBundle() + } + + return +} + +// Verify verifies if the downloaded files match the corresponding hash. +func (d *Downloader) Verify() error { + err := d.parseBundle() + if err != nil { + return err + } + + return d.bundle.Verify(d.dir) +} + +func (d *Downloader) parseBundle() error { + indexFilepath := filepath.Join(d.dir, d.indexFile) + var err error + d.bundle, err = ParseBundle(indexFilepath) + if err != nil { + return err + } + + d.version, err = semver.NewVersion(d.bundle.Version) + if err != nil { + return err + } + return nil +} + +func (d *Downloader) downloadIndexFileFromURL(ctx context.Context, url string) error { + // Request the index file + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return fmt.Errorf("failed to create GET request to %s: %w", url, err) + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + + // Perform request + resp, err := d.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed GET request to %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + + // Check the status code + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("received error from the server status code: %s", resp.Status) + } + // Create file + indexFilepath := filepath.Join(d.dir, d.indexFile) + file, err := os.Create(indexFilepath) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + // Write body of the response + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + return nil +} + +// CopyMatchingFilesFromCurrent check if there the current bundle files has matching files with the new bundle and copies them if they match. +func (d *Downloader) copyMatchingFilesFromCurrent(currentFiles map[string]File) error { + // Make sure new dir exists + _ = os.MkdirAll(d.dir, defaultDirMode) + + for _, a := range d.bundle.Artifacts { + currentFile, ok := currentFiles[a.Filename] + if ok && currentFile.Sha256() == a.SHA256 { + // Read the content of the current file. + content, err := os.ReadFile(currentFile.Path()) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", currentFile.Path(), err) + } + + // Check if the content matches the artifact hash + expectedHash, err := hex.DecodeString(a.SHA256) + if err != nil || len(expectedHash) != sha256.Size { + return fmt.Errorf("invalid artifact hash %s: %w", a.SHA256, err) + } + hash := sha256.Sum256(content) + if !bytes.Equal(expectedHash, hash[:]) { + return fmt.Errorf("expected and file hash mismatch: %s", currentFile.Path()) + } + + // Create new file + destFilePath := filepath.Join(d.dir, a.Filename) + err = os.WriteFile(destFilePath, content, a.GetFileMode()) + if err != nil { + return fmt.Errorf("failed to write to file %s: %w", destFilePath, err) + } + log.Debugf("updates: file copied from current version: %s", a.Filename) + } + } + return nil +} + +func (d *Downloader) downloadAndVerify(ctx context.Context) error { + // Make sure we have the bundle file parsed. + err := d.parseBundle() + if err != nil { + log.Errorf("updates: invalid update bundle file: %s", err) + } + + // Make sure dir exists + _ = os.MkdirAll(d.dir, defaultDirMode) + + for _, artifact := range d.bundle.Artifacts { + filePath := filepath.Join(d.dir, artifact.Filename) + + // Check file is already downloaded and valid. + exists, _ := checkIfFileIsValid(filePath, artifact) + if exists { + log.Debugf("updates: file already downloaded: %s", filePath) + continue + } + + // Download artifact + err := d.processArtifact(ctx, artifact, filePath) + if err != nil { + return err + } + } + return nil +} + +func (d *Downloader) processArtifact(ctx context.Context, artifact Artifact, filePath string) error { + providedHash, err := hex.DecodeString(artifact.SHA256) + if err != nil || len(providedHash) != sha256.Size { + return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) + } + + // Download + log.Debugf("updates: downloading file: %s", artifact.Filename) + content, err := d.downloadFile(ctx, artifact.URLs) + if err != nil { + return fmt.Errorf("failed to download artifact: %w", err) + } + + // Decompress + if artifact.Unpack != "" { + content, err = unpack(artifact.Unpack, content) + if err != nil { + return fmt.Errorf("failed to decompress artifact: %w", err) + } + } + + // Verify + hash := sha256.Sum256(content) + if !bytes.Equal(providedHash, hash[:]) { + return fmt.Errorf("failed to verify artifact: %s", artifact.Filename) + } + + // Save + tmpFilename := fmt.Sprintf("%s.download", filePath) + err = os.WriteFile(tmpFilename, content, artifact.GetFileMode()) + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + + // Rename + err = os.Rename(tmpFilename, filePath) + if err != nil { + return fmt.Errorf("failed to rename file: %w", err) + } + + log.Infof("updates: file downloaded and verified: %s", artifact.Filename) + + return nil +} + +func (d *Downloader) downloadFile(ctx context.Context, urls []string) ([]byte, error) { + for _, url := range urls { + // Try to make the request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + log.Warningf("failed to create GET request to %s: %s", url, err) + continue + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + resp, err := d.httpClient.Do(req) + if err != nil { + log.Warningf("failed a get file request to: %s", err) + continue + } + defer func() { _ = resp.Body.Close() }() + + // Check if the server returned an error + if resp.StatusCode != http.StatusOK { + log.Warningf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) + continue + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + log.Warningf("failed to read body of response: %s", err) + continue + } + return content, nil + } + + return nil, fmt.Errorf("failed to download file from the provided urls") +} + +func (d *Downloader) deleteUnfinishedDownloads() error { + entries, err := os.ReadDir(d.dir) + if err != nil { + return err + } + for _, e := range entries { + // Check if the current file has the download extension + if !e.IsDir() && strings.HasSuffix(e.Name(), ".download") { + path := filepath.Join(d.dir, e.Name()) + log.Warningf("updates: deleting unfinished download file: %s\n", path) + err := os.Remove(path) + if err != nil { + log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err) + } + } + } + return nil +} + +func unpack(cType string, fileBytes []byte) ([]byte, error) { + switch cType { + case "zip": + return decompressZip(fileBytes) + case "gz": + return decompressGzip(fileBytes) + default: + return nil, fmt.Errorf("unsupported compression type") + } +} + +func decompressGzip(data []byte) ([]byte, error) { + // Create a gzip reader from the byte array + gzipReader, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { _ = gzipReader.Close() }() + + var buf bytes.Buffer + _, err = io.CopyN(&buf, gzipReader, MaxUnpackSize) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("failed to read gzip file: %w", err) + } + + return buf.Bytes(), nil +} + +func decompressZip(data []byte) ([]byte, error) { + // Create a zip reader from the byte array + zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return nil, fmt.Errorf("failed to create zip reader: %w", err) + } + + // Ensure there is only one file in the zip + if len(zipReader.File) != 1 { + return nil, fmt.Errorf("zip file must contain exactly one file") + } + + // Read the single file in the zip + file := zipReader.File[0] + fileReader, err := file.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file in zip: %w", err) + } + defer func() { _ = fileReader.Close() }() + + var buf bytes.Buffer + _, err = io.CopyN(&buf, fileReader, MaxUnpackSize) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("failed to read file in zip: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/service/updates/index.go b/service/updates/index.go index c9a5f29d..de15a98b 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -1,77 +1 @@ package updates - -import ( - "context" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - - "github.com/safing/portmaster/base/log" -) - -type UpdateIndex struct { - Directory string - DownloadDirectory string - PurgeDirectory string - Ignore []string - IndexURLs []string - IndexFile string - AutoApply bool - NeedsRestart bool -} - -func (ui *UpdateIndex) DownloadIndexFile(ctx context.Context, client *http.Client) (err error) { - // Make sure dir exists - _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) - for _, url := range ui.IndexURLs { - err = ui.downloadIndexFileFromURL(ctx, client, url) - if err != nil { - log.Warningf("updates: failed while downloading index file %s", err) - continue - } - // Downloading was successful. - err = nil - break - } - return -} - -func (ui *UpdateIndex) downloadIndexFileFromURL(ctx context.Context, client *http.Client, url string) error { - // Request the index file - req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) - if err != nil { - return fmt.Errorf("failed to create GET request to %s: %w", url, err) - } - if UserAgent != "" { - req.Header.Set("User-Agent", UserAgent) - } - - // Perform request - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed GET request to %s: %w", url, err) - } - defer func() { _ = resp.Body.Close() }() - - // Check the status code - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("received error from the server status code: %s", resp.Status) - } - // Create file - filePath := filepath.Join(ui.DownloadDirectory, ui.IndexFile) - file, err := os.Create(filePath) - if err != nil { - return err - } - defer func() { _ = file.Close() }() - - // Write body of the response - _, err = io.Copy(file, resp.Body) - if err != nil { - return err - } - - return nil -} diff --git a/service/updates/module.go b/service/updates/module.go index 5caf2ab4..813d5025 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -1,15 +1,10 @@ package updates import ( - "context" - "errors" "fmt" - "net/http" - "os" "runtime" "time" - semver "github.com/hashicorp/go-version" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/log" @@ -21,12 +16,6 @@ const ( updateTaskRepeatDuration = 1 * time.Hour updateAvailableNotificationID = "updates:update-available" - // VersionUpdateEvent is emitted every time a new - // version of a monitored resource is selected. - // During module initialization VersionUpdateEvent - // is also emitted. - VersionUpdateEvent = "active version update" - // ResourceUpdateEvent is emitted every time the // updater successfully performed a resource update. // ResourceUpdateEvent is emitted even if no new @@ -34,6 +23,12 @@ const ( // to check if new versions of their resources are // available by checking File.UpgradeAvailable(). ResourceUpdateEvent = "resource update" + + // VersionUpdateEvent is emitted every time a new + // version of a monitored resource is selected. + // During module initialization VersionUpdateEvent + // is also emitted. + VersionUpdateEvent = "active version update" ) // UserAgent is an HTTP User-Agent that is used to add @@ -41,25 +36,18 @@ const ( // fetching resources from the update server. var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) -type File struct { - id string - path string +// UpdateIndex holds the configuration for the updates module +type UpdateIndex struct { + Directory string + DownloadDirectory string + PurgeDirectory string + Ignore []string + IndexURLs []string + IndexFile string + AutoApply bool + NeedsRestart bool } -func (f *File) Identifier() string { - return f.id -} - -func (f *File) Path() string { - return f.path -} - -func (f *File) Version() string { - return "" -} - -var ErrNotFound error = errors.New("file not found") - // Updates provides access to released artifacts. type Updates struct { m *mgr.Manager @@ -71,12 +59,11 @@ type Updates struct { EventResourcesUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}] - updateIndex UpdateIndex + registry Registry + downloader Downloader - bundle *Bundle - updateBundle *Bundle - - files map[string]File + autoApply bool + needsRestart bool instance instance } @@ -91,8 +78,8 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), - updateIndex: index, - files: make(map[string]File), + autoApply: index.AutoApply, + needsRestart: index.NeedsRestart, instance: instance, } @@ -103,53 +90,39 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", module.applyUpdates, nil) var err error - module.bundle, err = ParseBundle(module.updateIndex.Directory, module.updateIndex.IndexFile) + module.registry, err = CreateRegistry(index) if err != nil { - return nil, fmt.Errorf("failed to parse binary bundle: %s", err) + return nil, fmt.Errorf("failed to create registry: %w", err) } - // Add bundle artifacts to registry. - module.processBundle(module.bundle) + module.downloader = CreateDownloader(index) return module, nil } -func (reg *Updates) processBundle(bundle *Bundle) { - for _, artifact := range bundle.Artifacts { - artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename) - reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath} - } -} - func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { - httpClient := http.Client{} - err := u.updateIndex.DownloadIndexFile(wc.Ctx(), &httpClient) + err := u.downloader.downloadIndexFile(wc.Ctx()) if err != nil { - return fmt.Errorf("failed to download index file: %s", err) + return fmt.Errorf("failed to download index file: %w", err) } - u.updateBundle, err = ParseBundle(u.updateIndex.DownloadDirectory, u.updateIndex.IndexFile) - if err != nil { - return fmt.Errorf("failed parsing bundle: %s", err) - } defer u.EventResourcesUpdated.Submit(struct{}{}) - hasUpdate, err := u.checkVersionIncrement() - if err != nil { - return fmt.Errorf("failed to compare versions: %s", err) - } - - if !hasUpdate { + if u.downloader.version.LessThanOrEqual(u.registry.version) { log.Infof("updates: check compete: no new updates") return nil } - - log.Infof("updates: check complete: downloading new version: %s %s", u.updateBundle.Name, u.updateBundle.Version) - err = u.downloadUpdates(wc.Ctx(), &httpClient) + downloadBundle := u.downloader.bundle + log.Infof("updates: check complete: downloading new version: %s %s", downloadBundle.Name, downloadBundle.Version) + err = u.downloader.copyMatchingFilesFromCurrent(u.registry.files) if err != nil { - log.Errorf("updates: failed to download bundle: %s", err) + log.Warningf("updates: failed to copy files from current installation: %s", err) + } + err = u.downloader.downloadAndVerify(wc.Ctx()) + if err != nil { + log.Errorf("updates: failed to download update: %s", err) } else { - if u.updateIndex.AutoApply { + if u.autoApply { u.upgraderWorkerMgr.Go() } else { notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{ @@ -166,72 +139,28 @@ func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { return nil } -func (u *Updates) checkVersionIncrement() (bool, error) { - // Compare current and downloaded index version. - currentVersion, err := semver.NewVersion(u.bundle.Version) - if err != nil { - return false, err - } - downloadVersion, err := semver.NewVersion(u.updateBundle.Version) - if err != nil { - return false, err - } - log.Debugf("updates: checking version: curr: %s new: %s", currentVersion.String(), downloadVersion.String()) - return downloadVersion.GreaterThan(currentVersion), nil -} - -func (u *Updates) downloadUpdates(ctx context.Context, client *http.Client) error { - if u.updateBundle == nil { - // checkForUpdates needs to be called before this. - return fmt.Errorf("no valid update bundle found") - } - _ = deleteUnfinishedDownloads(u.updateIndex.DownloadDirectory) - err := u.updateBundle.CopyMatchingFilesFromCurrent(*u.bundle, u.updateIndex.Directory, u.updateIndex.DownloadDirectory) - if err != nil { - log.Warningf("updates: error while coping file from current to update: %s", err) - } - u.updateBundle.DownloadAndVerify(ctx, client, u.updateIndex.DownloadDirectory) - return nil -} - func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error { - // Check if we have new version - hasNewVersion, err := u.checkVersionIncrement() - if err != nil { - return fmt.Errorf("error while reading bundle version: %w", err) - } - - if !hasNewVersion { - return fmt.Errorf("there is no new version to apply") - } - - // Verify files of the downloaded files. - err = u.updateBundle.Verify(u.updateIndex.DownloadDirectory) - if err != nil { - return fmt.Errorf("failed to verify downloaded files: %s", err) - } - - // New version is downloaded and verified. Start the update process - log.Infof("update: starting update: %s %s -> %s", u.bundle.Name, u.bundle.Version, u.updateBundle.Version) - err = switchFolders(u.updateIndex, *u.updateBundle) + currentBundle := u.registry.bundle + downloadBundle := u.downloader.bundle + log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) + err := u.registry.performUpgrade(u.downloader.dir, u.downloader.indexFile) if err != nil { // TODO(vladimir): Send notification to UI log.Errorf("updates: failed to apply updates: %s", err) - } else { + } else if u.needsRestart { // TODO(vladimir): Prompt user to restart? - if u.updateIndex.NeedsRestart { - u.instance.Restart() - } + u.instance.Restart() } + u.EventResourcesUpdated.Submit(struct{}{}) return nil } -// TriggerUpdateCheck triggers an update check +// TriggerUpdateCheck triggers an update check. func (u *Updates) TriggerUpdateCheck() { u.updateCheckWorkerMgr.Go() } -// TriggerApplyUpdates triggers upgrade +// TriggerApplyUpdates triggers upgrade. func (u *Updates) TriggerApplyUpdates() { u.upgraderWorkerMgr.Go() } @@ -250,10 +179,8 @@ func (u *Updates) Manager() *mgr.Manager { func (u *Updates) Start() error { // Remove old files u.m.Go("old files cleaner", func(ctx *mgr.WorkerCtx) error { - err := os.RemoveAll(u.updateIndex.PurgeDirectory) - if err != nil { - return fmt.Errorf("failed to delete folder: %w", err) - } + _ = u.registry.CleanOldFiles() + _ = u.downloader.deleteUnfinishedDownloads() return nil }) u.updateCheckWorkerMgr.Go() @@ -261,15 +188,13 @@ func (u *Updates) Start() error { return nil } +// GetFile returns the path of a file given the name. func (u *Updates) GetFile(id string) (*File, error) { - file, ok := u.files[id] + file, ok := u.registry.files[id] if ok { return &file, nil } else { log.Errorf("updates: requested file id not found: %s", id) - for _, file := range u.files { - log.Debugf("File: %s", file) - } return nil, ErrNotFound } } diff --git a/service/updates/registry.go b/service/updates/registry.go new file mode 100644 index 00000000..72248508 --- /dev/null +++ b/service/updates/registry.go @@ -0,0 +1,177 @@ +package updates + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + + semver "github.com/hashicorp/go-version" + "github.com/safing/portmaster/base/log" +) + +const ( + defaultFileMode = os.FileMode(0o0644) + executableFileMode = os.FileMode(0o0744) + executableUIFileMode = os.FileMode(0o0755) + defaultDirMode = os.FileMode(0o0755) +) + +type Registry struct { + bundle *Bundle + dir string + purgeDir string + files map[string]File + + version *semver.Version +} + +func CreateRegistry(index UpdateIndex) (Registry, error) { + registry := Registry{ + dir: index.Directory, + purgeDir: index.PurgeDirectory, + files: make(map[string]File), + } + // Parse bundle + var err error + registry.bundle, err = ParseBundle(filepath.Join(index.Directory, index.IndexFile)) + if err != nil { + return Registry{}, err + } + + // Parse version + registry.version, err = semver.NewVersion(registry.bundle.Version) + if err != nil { + log.Errorf("updates: failed to parse current version: %s", err) + } + + // Process files + for _, artifact := range registry.bundle.Artifacts { + artifactPath := filepath.Join(registry.dir, artifact.Filename) + registry.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath, version: registry.bundle.Version, sha256: artifact.SHA256} + } + return registry, nil +} + +func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { + // Make sure provided update is valid + indexFilepath := filepath.Join(downloadDir, indexFile) + bundle, err := ParseBundle(indexFilepath) + if err != nil { + return fmt.Errorf("invalid update: %w", err) + } + + err = bundle.Verify(downloadDir) + if err != nil { + return fmt.Errorf("invalid update: %w", err) + } + + // Create purge dir. + err = os.MkdirAll(r.purgeDir, defaultDirMode) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + // Read all files in the current version folder. + files, err := os.ReadDir(r.dir) + if err != nil { + return err + } + + // Move current version files into purge folder. + log.Debugf("updates: removing the old version") + for _, file := range files { + currentFilepath := filepath.Join(r.dir, file.Name()) + purgePath := filepath.Join(r.purgeDir, file.Name()) + err := os.Rename(currentFilepath, purgePath) + if err != nil { + return fmt.Errorf("failed to move file %s: %w", currentFilepath, err) + } + } + + // Move the new index file + log.Debugf("updates: installing the new version") + newIndexFile := filepath.Join(r.dir, indexFile) + err = os.Rename(indexFilepath, newIndexFile) + if err != nil { + return fmt.Errorf("failed to move index file %s: %w", indexFile, err) + } + + // Move downloaded files to the current version folder. + for _, artifact := range bundle.Artifacts { + fromFilepath := filepath.Join(downloadDir, artifact.Filename) + toFilepath := filepath.Join(r.dir, artifact.Filename) + err = os.Rename(fromFilepath, toFilepath) + if err != nil { + log.Errorf("failed to move file %s: %s", fromFilepath, err) + } else { + log.Debugf("updates: %s moved", artifact.Filename) + } + + // Special case for linux. + // When installed the portmaster ui path is `/usr/bin/portmaster`. During update the ui will be placed in `/usr/lib/portmaster/portmaster` + // After an update the original binary should be deleted and replaced by symlink + // `/usr/bin/portmaster` -> `/usr/lib/portmaster/portmaster` + if runtime.GOOS == "linux" && artifact.Filename == "portmaster" && artifact.Platform == currentPlatform { + err = r.makeSymlinkForUI() + if err != nil { + log.Errorf("failed to create symlink for the ui: %s", err) + } else { + log.Infof("updates: ui symlink successfully created") + } + } + } + + log.Infof("updates: update complete") + + err = r.CleanOldFiles() + if err != nil { + log.Warningf("updates: error while cleaning old file: %s", err) + } + + return nil +} + +func (r *Registry) CleanOldFiles() error { + err := os.RemoveAll(r.purgeDir) + if err != nil { + return fmt.Errorf("failed to delete folder: %w", err) + } + return nil +} + +func (r *Registry) makeSymlinkForUI() error { + portmasterBinPath := "/usr/bin/portmaster" + _ = os.Remove(portmasterBinPath) + err := os.Symlink(filepath.Join(r.dir, "portmaster"), portmasterBinPath) + if err != nil { + return fmt.Errorf("failed to create symlink: %w", err) + } + return nil +} + +type File struct { + id string + path string + version string + sha256 string +} + +func (f *File) Identifier() string { + return f.id +} + +func (f *File) Path() string { + return f.path +} + +func (f *File) Version() string { + return f.version +} + +func (f *File) Sha256() string { + return f.sha256 +} + +var ErrNotFound error = errors.New("file not found") diff --git a/service/updates/updater.go b/service/updates/updater.go deleted file mode 100644 index f76ed2b9..00000000 --- a/service/updates/updater.go +++ /dev/null @@ -1,107 +0,0 @@ -package updates - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/safing/portmaster/base/log" -) - -const ( - defaultFileMode = os.FileMode(0o0644) - executableFileMode = os.FileMode(0o0744) - defaultDirMode = os.FileMode(0o0755) -) - -func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { - // Create purge dir. - err := os.MkdirAll(updateIndex.PurgeDirectory, defaultDirMode) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - - // Read all files in the current version folder. - files, err := os.ReadDir(updateIndex.Directory) - if err != nil { - return err - } - - // Move current version files into purge folder. - log.Debugf("updates: removing the old version") - for _, file := range files { - currentFilepath := filepath.Join(updateIndex.Directory, file.Name()) - purgePath := filepath.Join(updateIndex.PurgeDirectory, file.Name()) - err := os.Rename(currentFilepath, purgePath) - if err != nil { - return fmt.Errorf("failed to move file %s: %w", currentFilepath, err) - } - } - - // Move the new index file - log.Debugf("updates: installing the new version") - indexFile := filepath.Join(updateIndex.DownloadDirectory, updateIndex.IndexFile) - newIndexFile := filepath.Join(updateIndex.Directory, updateIndex.IndexFile) - err = os.Rename(indexFile, newIndexFile) - if err != nil { - return fmt.Errorf("failed to move index file %s: %w", indexFile, err) - } - - // Move downloaded files to the current version folder. - for _, artifact := range newBundle.Artifacts { - fromFilepath := filepath.Join(updateIndex.DownloadDirectory, artifact.Filename) - toFilepath := filepath.Join(updateIndex.Directory, artifact.Filename) - err = os.Rename(fromFilepath, toFilepath) - if err != nil { - log.Errorf("failed to move file %s: %s", fromFilepath, err) - } else { - log.Debugf("updates: %s moved", artifact.Filename) - } - - // Special case for linux. - // When installed the portmaster ui path is `/usr/bin/portmaster`. During update the ui will be placed in `/usr/lib/portmaster/portmaster` - // After an update the original binary should be deleted and replaced by symlink - // `/usr/bin/portmaster` -> `/usr/lib/portmaster/portmaster` - if runtime.GOOS == "linux" && artifact.Filename == "portmaster" && artifact.Platform == currentPlatform { - err = makeSymlinkForUI(updateIndex.Directory) - if err != nil { - log.Errorf("failed to create symlink for the ui: %s", err) - } else { - log.Infof("ui symlink successfully created") - } - } - } - - log.Debugf("updates: update complete") - - return nil -} - -func deleteUnfinishedDownloads(rootDir string) error { - entries, err := os.ReadDir(rootDir) - if err != nil { - return err - } - for _, e := range entries { - // Check if the current file has the download extension - if !e.IsDir() && strings.HasSuffix(e.Name(), ".download") { - path := filepath.Join(rootDir, e.Name()) - log.Warningf("updates: deleting unfinished download file: %s\n", path) - err := os.Remove(path) - if err != nil { - log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err) - } - } - } - return nil -} - -func makeSymlinkForUI(directory string) error { - err := os.Symlink(filepath.Join(directory, "portmaster"), "/usr/bin/portmaster") - if err != nil { - return fmt.Errorf("failed to create symlink: %w", err) - } - return nil -} From 1b6ee722f331c68892349fde9dbb236398e50abb Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 25 Sep 2024 16:33:45 +0300 Subject: [PATCH 16/62] [WIP] Fix edge upgrade edge cases --- desktop/tauri/src-tauri/templates/files.wxs | 11 ++- .../templates/nsis_install_hooks.nsh | 33 +++++-- desktop/tauri/src-tauri/templates/service.wxs | 2 +- service/instance.go | 10 +- service/updates/module.go | 4 +- service/updates/registry.go | 93 ++++++++++++++++--- 6 files changed, 124 insertions(+), 29 deletions(-) diff --git a/desktop/tauri/src-tauri/templates/files.wxs b/desktop/tauri/src-tauri/templates/files.wxs index 0eec08f1..d69ec0f9 100644 --- a/desktop/tauri/src-tauri/templates/files.wxs +++ b/desktop/tauri/src-tauri/templates/files.wxs @@ -1,14 +1,17 @@ - - - + + + + + + - + diff --git a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh index 9d7bd94a..a162a45d 100644 --- a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh +++ b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh @@ -1,13 +1,34 @@ -!define NSIS_HOOK_POSTINSTALL "NSIS_HOOK_POSTINSTALL_" +!macro NSIS_HOOK_PREINSTALL + ; Current working directory is \target\release\nsis\x64 + + SetOutPath "$INSTDIR" + + File "..\..\..\..\binaries\bin-index.json" + File "..\..\..\..\binaries\portmaster-core.exe" + File "..\..\..\..\binaries\portmaster-kext.sys" + File "..\..\..\..\binaries\portmaster.zip" + File "..\..\..\..\binaries\assets.zip" + + SetOutPath "$COMMONPROGRAMDATA\Portmaster\intel" + + File "..\..\..\..\binaries\intel-index.json" + File "..\..\..\..\binaries\base.dsdl" + File "..\..\..\..\binaries\geoipv4.mmdb" + File "..\..\..\..\binaries\geoipv6.mmdb" + File "..\..\..\..\binaries\index.dsd" + File "..\..\..\..\binaries\intermediate.dsdl" + File "..\..\..\..\binaries\urgent.dsdl" + + ; restire previous state + SetOutPath "$INSTDIR" -!macro NSIS_HOOK_POSTINSTALL_ - ExecWait '"$INSTDIR\portmaster-start.exe" install core-service --data="$INSTDIR\data"' !macroend +!macro NSIS_HOOK_POSTINSTALL + ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"' +!macroend -!define NSIS_HOOK_PREUNINSTALL "NSIS_HOOK_PREUNINSTALL_" - -!macro NSIS_HOOK_PREUNINSTALL_ +!macro NSIS_HOOK_PREUNINSTALL ExecWait 'sc.exe stop PortmasterCore' ExecWait 'sc.exe delete PortmasterCore' !macroend diff --git a/desktop/tauri/src-tauri/templates/service.wxs b/desktop/tauri/src-tauri/templates/service.wxs index 52f5339e..4ade87cd 100644 --- a/desktop/tauri/src-tauri/templates/service.wxs +++ b/desktop/tauri/src-tauri/templates/service.wxs @@ -3,7 +3,7 @@ %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) - err := u.registry.performUpgrade(u.downloader.dir, u.downloader.indexFile) + err := u.registry.performRecoverableUpgrade(u.downloader.dir, u.downloader.indexFile) if err != nil { // TODO(vladimir): Send notification to UI log.Errorf("updates: failed to apply updates: %s", err) diff --git a/service/updates/registry.go b/service/updates/registry.go index 72248508..35dc67be 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -3,6 +3,7 @@ package updates import ( "errors" "fmt" + "io" "os" "path/filepath" "runtime" @@ -67,11 +68,8 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { return fmt.Errorf("invalid update: %w", err) } - // Create purge dir. - err = os.MkdirAll(r.purgeDir, defaultDirMode) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } + // Make sure purge dir is empty. + _ = os.RemoveAll(r.purgeDir) // Read all files in the current version folder. files, err := os.ReadDir(r.dir) @@ -79,12 +77,18 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { return err } + // Create purge dir. Calling this after ReadDIr is important. + err = os.MkdirAll(r.purgeDir, defaultDirMode) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + // Move current version files into purge folder. log.Debugf("updates: removing the old version") for _, file := range files { currentFilepath := filepath.Join(r.dir, file.Name()) purgePath := filepath.Join(r.purgeDir, file.Name()) - err := os.Rename(currentFilepath, purgePath) + err := moveFile(currentFilepath, purgePath) if err != nil { return fmt.Errorf("failed to move file %s: %w", currentFilepath, err) } @@ -93,7 +97,7 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { // Move the new index file log.Debugf("updates: installing the new version") newIndexFile := filepath.Join(r.dir, indexFile) - err = os.Rename(indexFilepath, newIndexFile) + err = moveFile(indexFilepath, newIndexFile) if err != nil { return fmt.Errorf("failed to move index file %s: %w", indexFile, err) } @@ -102,9 +106,9 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { for _, artifact := range bundle.Artifacts { fromFilepath := filepath.Join(downloadDir, artifact.Filename) toFilepath := filepath.Join(r.dir, artifact.Filename) - err = os.Rename(fromFilepath, toFilepath) + err = moveFile(fromFilepath, toFilepath) if err != nil { - log.Errorf("failed to move file %s: %s", fromFilepath, err) + return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) } else { log.Debugf("updates: %s moved", artifact.Filename) } @@ -125,9 +129,76 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { log.Infof("updates: update complete") - err = r.CleanOldFiles() + return nil +} + +func moveFile(currentPath, newPath string) error { + err := os.Rename(currentPath, newPath) + if err == nil { + // Moving was successful return + return nil + } + + log.Debugf("updates: failed to move '%s' fallback to copy+delete: %s -> %s", err, currentPath, newPath) + + // Failed to move, try copy and delete + currentFile, err := os.Open(currentPath) if err != nil { - log.Warningf("updates: error while cleaning old file: %s", err) + return err + } + defer func() { _ = currentFile.Close() }() + + newFile, err := os.Create(newPath) + if err != nil { + return err + } + defer func() { _ = newFile.Close() }() + + _, err = io.Copy(newFile, currentFile) + if err != nil { + return err + } + + // Make sure file is closed before deletion. + _ = currentFile.Close() + currentFile = nil + + err = os.Remove(currentPath) + if err != nil { + log.Errorf("updates: failed to delete while moving file: %s", err) + } + + return nil +} + +func (r *Registry) performRecoverableUpgrade(downloadDir string, indexFile string) error { + upgradeError := r.performUpgrade(downloadDir, indexFile) + if upgradeError != nil { + err := r.recover() + recoverStatus := "(recovery successful)" + if err != nil { + recoverStatus = "(recovery failed)" + log.Errorf("updates: failed to recover: %s", err) + } + + return fmt.Errorf("upgrade failed: %w %s", upgradeError, recoverStatus) + } + return nil +} + +func (r *Registry) recover() error { + files, err := os.ReadDir(r.purgeDir) + if err != nil { + return err + } + + for _, file := range files { + recoverPath := filepath.Join(r.purgeDir, file.Name()) + currentFilepath := filepath.Join(r.dir, file.Name()) + err := moveFile(recoverPath, currentFilepath) + if err != nil { + return err + } } return nil From 08830f29c6ba5bce22a52e5679b7703ac1a469c5 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 25 Sep 2024 17:34:23 +0300 Subject: [PATCH 17/62] [WIP] Fix ui api authentication --- desktop/tauri/src-tauri/src/main.rs | 3 +-- packaging/linux/postinst | 3 +++ service/firewall/api.go | 3 +-- service/firewall/module.go | 2 ++ service/process/module.go | 14 +++++++++++++- service/process/profile.go | 17 +++-------------- service/updates/module.go | 4 ++++ 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 7dd9df42..cce1b266 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -120,7 +120,6 @@ fn show_webview_not_installed_dialog() -> i32 { } fn main() { - env::set_var("GDK_BACKEND", "x11"); if tauri::webview_version().is_err() { std::process::exit(show_webview_not_installed_dialog()); } @@ -139,7 +138,7 @@ fn main() { // TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now. #[cfg(target_os = "windows")] - let log_target = if let Some(data_dir) = cli.data { + let log_target = if let Some(data_dir) = cli_args.data { tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }) } else { tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout) diff --git a/packaging/linux/postinst b/packaging/linux/postinst index ab056723..60e3d97e 100644 --- a/packaging/linux/postinst +++ b/packaging/linux/postinst @@ -11,6 +11,9 @@ if command -V semanage >/dev/null 2>&1; then restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || : fi +mv /usr/bin/portmaster /usr/lib/portmaster/portmaster +ln -s /usr/lib/portmaster/portmaster /usr/bin/portmaster + systemctl daemon-reload systemctl enable portmaster.service diff --git a/service/firewall/api.go b/service/firewall/api.go index f16b7acd..50bafa39 100644 --- a/service/firewall/api.go +++ b/service/firewall/api.go @@ -132,8 +132,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo var originalPid int // Get authenticated path. - // FIXME(vladimir): provide a better check for detecting filepath. Note there is exception on linux with portmaster ui. - authenticatedPath := "" // updates.RootPath() + authenticatedPath := module.instance.BinaryUpdates().GetRootPath() if authenticatedPath == "" { return false, fmt.Errorf(deniedMsgMisconfigured, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user } diff --git a/service/firewall/module.go b/service/firewall/module.go index 131d4cac..d6e38bca 100644 --- a/service/firewall/module.go +++ b/service/firewall/module.go @@ -16,6 +16,7 @@ import ( "github.com/safing/portmaster/service/netquery" "github.com/safing/portmaster/service/network" "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/spn/access" "github.com/safing/portmaster/spn/captain" ) @@ -160,6 +161,7 @@ func New(instance instance) (*Firewall, error) { type instance interface { Config() *config.Config + BinaryUpdates() *updates.Updates Profile() *profile.ProfileModule Captain() *captain.Captain Access() *access.Access diff --git a/service/process/module.go b/service/process/module.go index b17a0c05..e2c6eb7f 100644 --- a/service/process/module.go +++ b/service/process/module.go @@ -4,12 +4,16 @@ import ( "errors" "sync/atomic" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/service/updates" ) type ProcessModule struct { mgr *mgr.Manager instance instance + + portmasterUIPath string } func (pm *ProcessModule) Manager() *mgr.Manager { @@ -17,6 +21,12 @@ func (pm *ProcessModule) Manager() *mgr.Manager { } func (pm *ProcessModule) Start() error { + file, err := pm.instance.BinaryUpdates().GetFile("portmaster") + if err != nil { + log.Errorf("process: failed to get path of ui: %s", err) + } else { + pm.portmasterUIPath = file.Path() + } return nil } @@ -59,4 +69,6 @@ func New(instance instance) (*ProcessModule, error) { return module, nil } -type instance interface{} +type instance interface { + BinaryUpdates() *updates.Updates +} diff --git a/service/process/profile.go b/service/process/profile.go index 7ac4ed15..e97b1d01 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -72,20 +72,9 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) { specialProfileID = profile.PortmasterProfileID default: // Check if this is another Portmaster component. - // FIXME(vladimir): provide a better check for detecting filepath. Note there is exception on linux with portmaster ui. - // if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) { - // switch { - // case strings.Contains(p.Path, "portmaster-app"): - // specialProfileID = profile.PortmasterAppProfileID - // case strings.Contains(p.Path, "portmaster-notifier"): - // specialProfileID = profile.PortmasterNotifierProfileID - // default: - // // Unexpected binary from within the Portmaster updates directpry. - // log.Warningf("process: unexpected binary in the updates directory: %s", p.Path) - // // TODO: Assign a fully restricted profile in the future when we are - // // sure that we won't kill any of our own things. - // } - // } + if module.portmasterUIPath != "" && p.Path == module.portmasterUIPath { + specialProfileID = profile.PortmasterAppProfileID + } // Check if this is the system resolver. switch runtime.GOOS { case "windows": diff --git a/service/updates/module.go b/service/updates/module.go index 8807aa24..16858836 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -188,6 +188,10 @@ func (u *Updates) Start() error { return nil } +func (u *Updates) GetRootPath() string { + return u.registry.dir +} + // GetFile returns the path of a file given the name. func (u *Updates) GetFile(id string) (*File, error) { file, ok := u.registry.files[id] From 7cf20b2cf81a57500281d713db5c591446659baf Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 25 Sep 2024 17:42:05 +0300 Subject: [PATCH 18/62] [WIP] Remove old code --- base/updater/doc.go | 2 - base/updater/export.go | 15 - base/updater/fetch.go | 347 -------------------- base/updater/file.go | 148 --------- base/updater/filename.go | 57 ---- base/updater/filename_test.go | 80 ----- base/updater/get.go | 87 ----- base/updater/indexes.go | 109 ------- base/updater/indexes_test.go | 57 ---- base/updater/notifier.go | 33 -- base/updater/registry.go | 270 ---------------- base/updater/registry_test.go | 35 -- base/updater/resource.go | 582 ---------------------------------- base/updater/resource_test.go | 119 ------- base/updater/signing.go | 49 --- base/updater/state.go | 180 ----------- base/updater/storage.go | 272 ---------------- base/updater/storage_test.go | 68 ---- base/updater/unpacking.go | 195 ------------ base/updater/updating.go | 359 --------------------- service/updates/registry.go | 24 -- 21 files changed, 3088 deletions(-) delete mode 100644 base/updater/doc.go delete mode 100644 base/updater/export.go delete mode 100644 base/updater/fetch.go delete mode 100644 base/updater/file.go delete mode 100644 base/updater/filename.go delete mode 100644 base/updater/filename_test.go delete mode 100644 base/updater/get.go delete mode 100644 base/updater/indexes.go delete mode 100644 base/updater/indexes_test.go delete mode 100644 base/updater/notifier.go delete mode 100644 base/updater/registry.go delete mode 100644 base/updater/registry_test.go delete mode 100644 base/updater/resource.go delete mode 100644 base/updater/resource_test.go delete mode 100644 base/updater/signing.go delete mode 100644 base/updater/state.go delete mode 100644 base/updater/storage.go delete mode 100644 base/updater/storage_test.go delete mode 100644 base/updater/unpacking.go delete mode 100644 base/updater/updating.go diff --git a/base/updater/doc.go b/base/updater/doc.go deleted file mode 100644 index 829a5bd3..00000000 --- a/base/updater/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package updater is an update registry that manages updates and versions. -package updater diff --git a/base/updater/export.go b/base/updater/export.go deleted file mode 100644 index 51889bde..00000000 --- a/base/updater/export.go +++ /dev/null @@ -1,15 +0,0 @@ -package updater - -// // Export exports the list of resources. -// func (reg *ResourceRegistry) Export() map[string]*Resource { -// reg.RLock() -// defer reg.RUnlock() - -// // copy the map -// copiedResources := make(map[string]*Resource) -// for key, val := range reg.resources { -// copiedResources[key] = val.Export() -// } - -// return copiedResources -// } diff --git a/base/updater/fetch.go b/base/updater/fetch.go deleted file mode 100644 index 89456774..00000000 --- a/base/updater/fetch.go +++ /dev/null @@ -1,347 +0,0 @@ -package updater - -// import ( -// "bytes" -// "context" -// "errors" -// "fmt" -// "hash" -// "io" -// "net/http" -// "net/url" -// "os" -// "path" -// "path/filepath" -// "time" - -// "github.com/safing/jess/filesig" -// "github.com/safing/jess/lhash" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils/renameio" -// ) - -// func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { -// // backoff when retrying -// if tries > 0 { -// select { -// case <-ctx.Done(): -// return nil // module is shutting down -// case <-time.After(time.Duration(tries*tries) * time.Second): -// } -// } - -// // check destination dir -// dirPath := filepath.Dir(rv.storagePath()) -// err := reg.storageDir.EnsureAbsPath(dirPath) -// if err != nil { -// return fmt.Errorf("could not create updates folder: %s", dirPath) -// } - -// // If verification is enabled, download signature first. -// var ( -// verifiedHash *lhash.LabeledHash -// sigFileData []byte -// ) -// if rv.resource.VerificationOptions != nil { -// verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( -// ctx, client, -// rv.resource.VerificationOptions, -// rv.versionedSigPath(), rv.SigningMetadata(), -// tries, -// ) -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("signature verification failed: %w", err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// } -// } -// } - -// // open file for writing -// atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) -// if err != nil { -// return fmt.Errorf("could not create temp file for download: %w", err) -// } -// defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - -// // start file download -// resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) -// if err != nil { -// return err -// } -// defer func() { -// _ = resp.Body.Close() -// }() - -// // Write to the hasher at the same time, if needed. -// var hasher hash.Hash -// var writeDst io.Writer = atomicFile -// if verifiedHash != nil { -// hasher = verifiedHash.Algorithm().RawHasher() -// writeDst = io.MultiWriter(hasher, atomicFile) -// } - -// // Download and write file. -// n, err := io.Copy(writeDst, resp.Body) -// if err != nil { -// return fmt.Errorf("failed to download %q: %w", downloadURL, err) -// } -// if resp.ContentLength != n { -// return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) -// } - -// // Before file is finalized, check if hash, if available. -// if hasher != nil { -// downloadDigest := hasher.Sum(nil) -// if verifiedHash.EqualRaw(downloadDigest) { -// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) -// } else { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return errors.New("file does not match signed checksum") -// case SignaturePolicyWarn: -// log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) -// case SignaturePolicyDisable: -// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) -// } - -// // Reset hasher to signal that the sig should not be written. -// hasher = nil -// } -// } - -// // Write signature file, if we have one and if verification succeeded. -// if len(sigFileData) > 0 && hasher != nil { -// sigFilePath := rv.storagePath() + filesig.Extension -// err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) -// } -// } -// } - -// // finalize file -// err = atomicFile.CloseAtomicallyReplace() -// if err != nil { -// return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) -// } -// // set permissions -// if !onWindows { -// // TODO: only set executable files to 0755, set other to 0644 -// err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. -// if err != nil { -// log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) -// } -// } - -// log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) -// return nil -// } - -// func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { -// // backoff when retrying -// if tries > 0 { -// select { -// case <-ctx.Done(): -// return nil // module is shutting down -// case <-time.After(time.Duration(tries*tries) * time.Second): -// } -// } - -// // Check destination dir. -// dirPath := filepath.Dir(rv.storagePath()) -// err := reg.storageDir.EnsureAbsPath(dirPath) -// if err != nil { -// return fmt.Errorf("could not create updates folder: %s", dirPath) -// } - -// // Download and verify the missing signature. -// verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( -// ctx, client, -// rv.resource.VerificationOptions, -// rv.versionedSigPath(), rv.SigningMetadata(), -// tries, -// ) -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("signature verification failed: %w", err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// } -// return nil -// } - -// // Check if the signature matches the resource file. -// ok, err := verifiedHash.MatchesFile(rv.storagePath()) -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("error while verifying resource file: %w", err) -// case SignaturePolicyWarn: -// log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) -// case SignaturePolicyDisable: -// log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) -// } -// return nil -// } -// if !ok { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return errors.New("resource file does not match signed checksum") -// case SignaturePolicyWarn: -// log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) -// case SignaturePolicyDisable: -// log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) -// } -// return nil -// } - -// // Write signature file. -// err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) -// } -// } - -// log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) -// return nil -// } - -// func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { -// // Download signature file. -// resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) -// if err != nil { -// return nil, nil, err -// } -// defer func() { -// _ = resp.Body.Close() -// }() -// sigFileData, err := io.ReadAll(resp.Body) -// if err != nil { -// return nil, nil, err -// } - -// // Extract all signatures. -// sigs, err := filesig.ParseSigFile(sigFileData) -// switch { -// case len(sigs) == 0 && err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// case len(sigs) == 0: -// return nil, nil, errors.New("no signatures found in signature file") -// case err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// } - -// // Verify all signatures. -// var verifiedHash *lhash.LabeledHash -// for _, sig := range sigs { -// fd, err := filesig.VerifyFileData( -// sig, -// requiredMetadata, -// verifOpts.TrustStore, -// ) -// if err != nil { -// return nil, sigFileData, err -// } - -// // Save or check verified hash. -// if verifiedHash == nil { -// verifiedHash = fd.FileHash() -// } else if !fd.FileHash().Equal(verifiedHash) { -// // Return an error if two valid hashes mismatch. -// // For simplicity, all hash algorithms must be the same for now. -// return nil, sigFileData, errors.New("file hashes from different signatures do not match") -// } -// } - -// return verifiedHash, sigFileData, nil -// } - -// func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { -// // backoff when retrying -// if tries > 0 { -// select { -// case <-ctx.Done(): -// return nil, "", nil // module is shutting down -// case <-time.After(time.Duration(tries*tries) * time.Second): -// } -// } - -// // start file download -// resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) -// if err != nil { -// return nil, downloadURL, err -// } -// defer func() { -// _ = resp.Body.Close() -// }() - -// // download and write file -// buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) -// n, err := io.Copy(buf, resp.Body) -// if err != nil { -// return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) -// } -// if resp.ContentLength != n { -// return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) -// } - -// return buf.Bytes(), downloadURL, nil -// } - -// func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { -// // parse update URL -// updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] -// u, err := url.Parse(updateBaseURL) -// if err != nil { -// return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) -// } -// // add download path -// u.Path = path.Join(u.Path, downloadPath) -// // compile URL -// downloadURL = u.String() - -// // create request -// req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) -// if err != nil { -// return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) -// } - -// // set user agent -// if reg.UserAgent != "" { -// req.Header.Set("User-Agent", reg.UserAgent) -// } - -// // start request -// resp, err = client.Do(req) -// if err != nil { -// return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) -// } - -// // check return code -// if resp.StatusCode != http.StatusOK { -// _ = resp.Body.Close() -// return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) -// } - -// return resp, downloadURL, err -// } diff --git a/base/updater/file.go b/base/updater/file.go deleted file mode 100644 index 758f2a7b..00000000 --- a/base/updater/file.go +++ /dev/null @@ -1,148 +0,0 @@ -package updater - -import ( - -// semver "github.com/hashicorp/go-version" - -) - -// File represents a file from the update system. -// type File struct { -// resource *Resource -// version *ResourceVersion -// notifier *notifier -// versionedPath string -// storagePath string -// } - -// // Identifier returns the identifier of the file. -// func (file *File) Identifier() string { -// return file.resource.Identifier -// } - -// // Version returns the version of the file. -// func (file *File) Version() string { -// return file.version.VersionNumber -// } - -// // SemVer returns the semantic version of the file. -// func (file *File) SemVer() *semver.Version { -// return file.version.semVer -// } - -// // EqualsVersion normalizes the given version and checks equality with semver. -// func (file *File) EqualsVersion(version string) bool { -// return file.version.EqualsVersion(version) -// } - -// // Path returns the absolute filepath of the file. -// func (file *File) Path() string { -// return file.storagePath -// } - -// // SigningMetadata returns the metadata to be included in signatures. -// func (file *File) SigningMetadata() map[string]string { -// return map[string]string{ -// "id": file.Identifier(), -// "version": file.Version(), -// } -// } - -// Verify verifies the given file. -// func (file *File) Verify() ([]*filesig.FileData, error) { -// // Check if verification is configured. -// if file.resource.VerificationOptions == nil { -// return nil, ErrVerificationNotConfigured -// } - -// // Verify file. -// fileData, err := filesig.VerifyFile( -// file.storagePath, -// file.storagePath+filesig.Extension, -// file.SigningMetadata(), -// file.resource.VerificationOptions.TrustStore, -// ) -// if err != nil { -// switch file.resource.VerificationOptions.DiskLoadPolicy { -// case SignaturePolicyRequire: -// return nil, err -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) -// } -// } - -// return fileData, nil -// } - -// Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted. -// func (file *File) Blacklist() error { -// return file.resource.Blacklist(file.version.VersionNumber) -// } - -// markActiveWithLocking marks the file as active, locking the resource in the process. -// func (file *File) markActiveWithLocking() { -// file.resource.Lock() -// defer file.resource.Unlock() - -// // update last used version -// if file.resource.ActiveVersion != file.version { -// log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) -// file.resource.ActiveVersion = file.version -// } -// } - -// Unpacker describes the function that is passed to -// File.Unpack. It receives a reader to the compressed/packed -// file and should return a reader that provides -// unpacked file contents. If the returned reader implements -// io.Closer it's close method is invoked when an error -// or io.EOF is returned from Read(). -// type Unpacker func(io.Reader) (io.Reader, error) - -// Unpack returns the path to the unpacked version of file and -// unpacks it on demand using unpacker. -// func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { -// path := strings.TrimSuffix(file.Path(), suffix) - -// if suffix == "" { -// path += "-unpacked" -// } - -// _, err := os.Stat(path) -// if err == nil { -// return path, nil -// } - -// if !errors.Is(err, fs.ErrNotExist) { -// return "", err -// } - -// f, err := os.Open(file.Path()) -// if err != nil { -// return "", err -// } -// defer func() { -// _ = f.Close() -// }() - -// r, err := unpacker(f) -// if err != nil { -// return "", err -// } - -// ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ -// TempDir: file.resource.registry.TmpDir().Path, -// }) - -// if c, ok := r.(io.Closer); ok { -// if err := c.Close(); err != nil && ioErr == nil { -// // if ioErr is already set we ignore the error from -// // closing the unpacker. -// ioErr = err -// } -// } - -// return path, ioErr -// } diff --git a/base/updater/filename.go b/base/updater/filename.go deleted file mode 100644 index 69e9db00..00000000 --- a/base/updater/filename.go +++ /dev/null @@ -1,57 +0,0 @@ -package updater - -import ( - "path" - "regexp" - "strings" -) - -var ( - fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`) - rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?$`) -) - -// GetIdentifierAndVersion splits the given file path into its identifier and version. -func GetIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) { - dirPath, filename := path.Split(versionedPath) - - // Extract version from filename. - rawVersion := fileVersionRegex.FindString(filename) - if rawVersion == "" { - // No version present in file, making it invalid. - return "", "", false - } - - // Trim the `_v` that gets caught by the regex and - // replace `-` with `.` to get the version string. - version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", 2) - - // Put the filename back together without version. - i := strings.Index(filename, rawVersion) - if i < 0 { - // extracted version not in string (impossible) - return "", "", false - } - filename = filename[:i] + filename[i+len(rawVersion):] - - // Put the full path back together and return it. - // `dirPath + filename` is guaranteed by path.Split() - return dirPath + filename, version, true -} - -// GetVersionedPath combines the identifier and version and returns it as a file path. -func GetVersionedPath(identifier, version string) (versionedPath string) { - identifierPath, filename := path.Split(identifier) - - // Split the filename where the version should go. - splittedFilename := strings.SplitN(filename, ".", 2) - // Replace `.` with `-` for the filename format. - transformedVersion := strings.Replace(version, ".", "-", 2) - - // Put everything back together and return it. - versionedPath = identifierPath + splittedFilename[0] + "_v" + transformedVersion - if len(splittedFilename) > 1 { - versionedPath += "." + splittedFilename[1] - } - return versionedPath -} diff --git a/base/updater/filename_test.go b/base/updater/filename_test.go deleted file mode 100644 index cf5fb922..00000000 --- a/base/updater/filename_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package updater - -import ( - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func testRegexMatch(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) { - t.Helper() - - if testRegex.MatchString(testString) != shouldMatch { - if shouldMatch { - t.Errorf("regex %s should match %s", testRegex, testString) - } else { - t.Errorf("regex %s should not match %s", testRegex, testString) - } - } -} - -func testRegexFind(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) { - t.Helper() - - if (testRegex.FindString(testString) != "") != shouldMatch { - if shouldMatch { - t.Errorf("regex %s should find %s", testRegex, testString) - } else { - t.Errorf("regex %s should not find %s", testRegex, testString) - } - } -} - -func testVersionTransformation(t *testing.T, testFilename, testIdentifier, testVersion string) { - t.Helper() - - identifier, version, ok := GetIdentifierAndVersion(testFilename) - if !ok { - t.Errorf("failed to get identifier and version of %s", testFilename) - } - assert.Equal(t, testIdentifier, identifier, "identifier does not match") - assert.Equal(t, testVersion, version, "version does not match") - - versionedPath := GetVersionedPath(testIdentifier, testVersion) - assert.Equal(t, testFilename, versionedPath, "filename (versioned path) does not match") -} - -func TestRegexes(t *testing.T) { - t.Parallel() - - testRegexMatch(t, rawVersionRegex, "0.1.2", true) - testRegexMatch(t, rawVersionRegex, "0.1.2-beta", true) - testRegexMatch(t, rawVersionRegex, "0.1.2-staging", true) - testRegexMatch(t, rawVersionRegex, "12.13.14", true) - - testRegexMatch(t, rawVersionRegex, "v0.1.2", false) - testRegexMatch(t, rawVersionRegex, "0.", false) - testRegexMatch(t, rawVersionRegex, "0.1", false) - testRegexMatch(t, rawVersionRegex, "0.1.", false) - testRegexMatch(t, rawVersionRegex, ".1.2", false) - testRegexMatch(t, rawVersionRegex, ".1.", false) - testRegexMatch(t, rawVersionRegex, "012345", false) - - testRegexFind(t, fileVersionRegex, "/path/to/file_v0-0-0", true) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3", true) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3.exe", true) - - testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1.2.3", false) - testRegexFind(t, fileVersionRegex, "/path/to/file_1-2-3", false) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2", false) - testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false) - - testVersionTransformation(t, "/path/to/file_v0-0-0", "/path/to/file", "0.0.0") - testVersionTransformation(t, "/path/to/file_v1-2-3", "/path/to/file", "1.2.3") - testVersionTransformation(t, "/path/to/file_v1-2-3-beta", "/path/to/file", "1.2.3-beta") - testVersionTransformation(t, "/path/to/file_v1-2-3-staging", "/path/to/file", "1.2.3-staging") - testVersionTransformation(t, "/path/to/file_v1-2-3.exe", "/path/to/file.exe", "1.2.3") - testVersionTransformation(t, "/path/to/file_v1-2-3-staging.exe", "/path/to/file.exe", "1.2.3-staging") -} diff --git a/base/updater/get.go b/base/updater/get.go deleted file mode 100644 index 365cb736..00000000 --- a/base/updater/get.go +++ /dev/null @@ -1,87 +0,0 @@ -package updater - -import ( - "errors" -) - -// Errors returned by the updater package. -var ( - ErrNotFound = errors.New("the requested file could not be found") - ErrNotAvailableLocally = errors.New("the requested file is not available locally") - ErrVerificationNotConfigured = errors.New("verification not configured for this resource") -) - -// GetFile returns the selected (mostly newest) file with the given -// identifier or an error, if it fails. -// func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { -// return nil, fmt.Errorf("invalid file: %s", identifier) -// reg.RLock() -// res, ok := reg.resources[identifier] -// reg.RUnlock() -// if !ok { -// return nil, ErrNotFound -// } - -// file := res.GetFile() -// // check if file is available locally -// if file.version.Available { -// file.markActiveWithLocking() - -// // Verify file, if configured. -// _, err := file.Verify() -// if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { -// // TODO: If verification is required, try deleting the resource and downloading it again. -// return nil, fmt.Errorf("failed to verify file: %w", err) -// } - -// return file, nil -// } - -// // check if online -// if !reg.Online { -// return nil, ErrNotAvailableLocally -// } - -// // check download dir -// err := reg.tmpDir.Ensure() -// if err != nil { -// return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) -// } - -// // Start registry operation. -// reg.state.StartOperation(StateFetching) -// defer reg.state.EndOperation() - -// // download file -// log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) -// client := &http.Client{} -// for tries := range 5 { -// err = reg.fetchFile(context.TODO(), client, file.version, tries) -// if err != nil { -// log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) -// } else { -// file.markActiveWithLocking() - -// // TODO: We just download the file - should we verify it again? -// return file, nil -// } -// } -// log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) -// return nil, err -// } - -// GetVersion returns the selected version of the given identifier. -// The returned resource version may not be modified. -// func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { -// reg.RLock() -// res, ok := reg.resources[identifier] -// reg.RUnlock() -// if !ok { -// return nil, ErrNotFound -// } - -// res.Lock() -// defer res.Unlock() - -// return res.SelectedVersion, nil -// } diff --git a/base/updater/indexes.go b/base/updater/indexes.go deleted file mode 100644 index 81a373a3..00000000 --- a/base/updater/indexes.go +++ /dev/null @@ -1,109 +0,0 @@ -package updater - -import ( - "encoding/json" - "errors" - "fmt" - "time" -) - -const ( - baseIndexExtension = ".json" - v2IndexExtension = ".v2.json" -) - -// Index describes an index file pulled by the updater. -type Index struct { - // Path is the path to the index file - // on the update server. - Path string - - // Channel holds the release channel name of the index. - // It must match the filename without extension. - Channel string - - // PreRelease signifies that all versions of this index should be marked as - // pre-releases, no matter if the versions actually have a pre-release tag or - // not. - PreRelease bool - - // AutoDownload specifies whether new versions should be automatically downloaded. - AutoDownload bool - - // LastRelease holds the time of the last seen release of this index. - LastRelease time.Time -} - -// IndexFile represents an index file. -type IndexFile struct { - Channel string - Published time.Time - - Releases map[string]string -} - -var ( - // ErrIndexChecksumMismatch is returned when an index does not match its - // signed checksum. - ErrIndexChecksumMismatch = errors.New("index checksum does mot match signature") - - // ErrIndexFromFuture is returned when an index is parsed with a - // Published timestamp that lies in the future. - ErrIndexFromFuture = errors.New("index is from the future") - - // ErrIndexIsOlder is returned when an index is parsed with an older - // Published timestamp than the current Published timestamp. - ErrIndexIsOlder = errors.New("index is older than the current one") - - // ErrIndexChannelMismatch is returned when an index is parsed with a - // different channel that the expected one. - ErrIndexChannelMismatch = errors.New("index does not match the expected channel") -) - -// ParseIndexFile parses an index file and checks if it is valid. -func ParseIndexFile(indexData []byte, channel string, lastIndexRelease time.Time) (*IndexFile, error) { - // Load into struct. - indexFile := &IndexFile{} - err := json.Unmarshal(indexData, indexFile) - if err != nil { - return nil, fmt.Errorf("failed to parse signed index data: %w", err) - } - - // Fallback to old format if there are no releases and no channel is defined. - // TODO: Remove in v1 - if len(indexFile.Releases) == 0 && indexFile.Channel == "" { - return loadOldIndexFormat(indexData, channel) - } - - // Check the index metadata. - switch { - case !indexFile.Published.IsZero() && time.Now().Before(indexFile.Published): - return indexFile, ErrIndexFromFuture - - case !indexFile.Published.IsZero() && - !lastIndexRelease.IsZero() && - lastIndexRelease.After(indexFile.Published): - return indexFile, ErrIndexIsOlder - - case channel != "" && - indexFile.Channel != "" && - channel != indexFile.Channel: - return indexFile, ErrIndexChannelMismatch - } - - return indexFile, nil -} - -func loadOldIndexFormat(indexData []byte, channel string) (*IndexFile, error) { - releases := make(map[string]string) - err := json.Unmarshal(indexData, &releases) - if err != nil { - return nil, err - } - - return &IndexFile{ - Channel: channel, - // Do NOT define `Published`, as this would break the "is newer" check. - Releases: releases, - }, nil -} diff --git a/base/updater/indexes_test.go b/base/updater/indexes_test.go deleted file mode 100644 index a85046cd..00000000 --- a/base/updater/indexes_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package updater - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - oldFormat = `{ - "all/ui/modules/assets.zip": "0.3.0", - "all/ui/modules/portmaster.zip": "0.2.4", - "linux_amd64/core/portmaster-core": "0.8.13" -}` - - newFormat = `{ - "Channel": "stable", - "Published": "2022-01-02T00:00:00Z", - "Releases": { - "all/ui/modules/assets.zip": "0.3.0", - "all/ui/modules/portmaster.zip": "0.2.4", - "linux_amd64/core/portmaster-core": "0.8.13" - } -}` - - formatTestChannel = "stable" - formatTestReleases = map[string]string{ - "all/ui/modules/assets.zip": "0.3.0", - "all/ui/modules/portmaster.zip": "0.2.4", - "linux_amd64/core/portmaster-core": "0.8.13", - } -) - -func TestIndexParsing(t *testing.T) { - t.Parallel() - - lastRelease, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") - if err != nil { - t.Fatal(err) - } - - oldIndexFile, err := ParseIndexFile([]byte(oldFormat), formatTestChannel, lastRelease) - if err != nil { - t.Fatal(err) - } - - newIndexFile, err := ParseIndexFile([]byte(newFormat), formatTestChannel, lastRelease) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, formatTestChannel, oldIndexFile.Channel, "channel should be the same") - assert.Equal(t, formatTestChannel, newIndexFile.Channel, "channel should be the same") - assert.Equal(t, formatTestReleases, oldIndexFile.Releases, "releases should be the same") - assert.Equal(t, formatTestReleases, newIndexFile.Releases, "releases should be the same") -} diff --git a/base/updater/notifier.go b/base/updater/notifier.go deleted file mode 100644 index 75de01e4..00000000 --- a/base/updater/notifier.go +++ /dev/null @@ -1,33 +0,0 @@ -package updater - -// import ( -// "github.com/tevino/abool" -// ) - -// type notifier struct { -// upgradeAvailable *abool.AtomicBool -// notifyChannel chan struct{} -// } - -// func newNotifier() *notifier { -// return ¬ifier{ -// upgradeAvailable: abool.NewBool(false), -// notifyChannel: make(chan struct{}), -// } -// } - -// func (n *notifier) markAsUpgradeable() { -// if n.upgradeAvailable.SetToIf(false, true) { -// close(n.notifyChannel) -// } -// } - -// // UpgradeAvailable returns whether an upgrade is available for this file. -// func (file *File) UpgradeAvailable() bool { -// return file.notifier.upgradeAvailable.IsSet() -// } - -// // WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. -// func (file *File) WaitForAvailableUpgrade() <-chan struct{} { -// return file.notifier.notifyChannel -// } diff --git a/base/updater/registry.go b/base/updater/registry.go deleted file mode 100644 index 3d4861bf..00000000 --- a/base/updater/registry.go +++ /dev/null @@ -1,270 +0,0 @@ -package updater - -// import ( -// "errors" -// "fmt" -// "os" -// "path/filepath" -// "runtime" -// "strings" -// "sync" - -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// const ( -// onWindows = runtime.GOOS == "windows" -// ) - -// ResourceRegistry is a registry for managing update resources. -// type ResourceRegistry struct { -// sync.RWMutex - -// Name string -// storageDir *utils.DirStructure -// tmpDir *utils.DirStructure -// indexes []*Index -// state *RegistryState - -// resources map[string]*Resource -// UpdateURLs []string -// UserAgent string -// MandatoryUpdates []string -// AutoUnpack []string - -// // Verification holds a map of VerificationOptions assigned to their -// // applicable identifier path prefix. -// // Use an empty string to denote the default. -// // Use empty options to disable verification for a path prefix. -// Verification map[string]*VerificationOptions - -// // UsePreReleases signifies that pre-releases should be used when selecting a -// // version. Even if false, a pre-release version will still be used if it is -// // defined as the current version by an index. -// UsePreReleases bool - -// // DevMode specifies if a local 0.0.0 version should be always chosen, when available. -// DevMode bool - -// // Online specifies if resources may be downloaded if not available locally. -// Online bool - -// // StateNotifyFunc may be set to receive any changes to the registry state. -// // The specified function may lock the state, but may not block or take a -// // lot of time. -// StateNotifyFunc func(*RegistryState) -// } - -// // AddIndex adds a new index to the resource registry. -// // The order is important, as indexes added later will override the current -// // release from earlier indexes. -// func (reg *ResourceRegistry) AddIndex(idx Index) { -// reg.Lock() -// defer reg.Unlock() - -// // Get channel name from path. -// idx.Channel = strings.TrimSuffix( -// filepath.Base(idx.Path), filepath.Ext(idx.Path), -// ) - -// reg.indexes = append(reg.indexes, &idx) -// } - -// // PreInitUpdateState sets the initial update state of the registry before initialization. -// func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { -// if reg.state != nil { -// return errors.New("registry already initialized") -// } - -// reg.state = &RegistryState{ -// Updates: s, -// } -// return nil -// } - -// // Initialize initializes a raw registry struct and makes it ready for usage. -// func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { -// // check if storage dir is available -// err := storageDir.Ensure() -// if err != nil { -// return err -// } - -// // set default name -// if reg.Name == "" { -// reg.Name = "updater" -// } - -// // initialize private attributes -// reg.storageDir = storageDir -// reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) -// reg.resources = make(map[string]*Resource) -// if reg.state == nil { -// reg.state = &RegistryState{} -// } -// reg.state.ID = StateReady -// reg.state.reg = reg - -// // remove tmp dir to delete old entries -// err = reg.Cleanup() -// if err != nil { -// log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) -// } - -// // (re-)create tmp dir -// err = reg.tmpDir.Ensure() -// if err != nil { -// log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) -// } - -// // Check verification options. -// if reg.Verification != nil { -// for prefix, opts := range reg.Verification { -// // Check if verification is disable for this prefix. -// if opts == nil { -// continue -// } - -// // If enabled, a trust store is required. -// if opts.TrustStore == nil { -// return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) -// } - -// // DownloadPolicy must be equal or stricter than DiskLoadPolicy. -// if opts.DiskLoadPolicy < opts.DownloadPolicy { -// return errors.New("verification download policy must be equal or stricter than the disk load policy") -// } - -// // Warn if all policies are disabled. -// if opts.DownloadPolicy == SignaturePolicyDisable && -// opts.DiskLoadPolicy == SignaturePolicyDisable { -// log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) -// } -// } -// } - -// return nil -// } - -// // StorageDir returns the main storage dir of the resource registry. -// func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { -// return reg.storageDir -// } - -// // TmpDir returns the temporary working dir of the resource registry. -// func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { -// return reg.tmpDir -// } - -// // SetDevMode sets the development mode flag. -// func (reg *ResourceRegistry) SetDevMode(on bool) { -// reg.Lock() -// defer reg.Unlock() - -// reg.DevMode = on -// } - -// // SetUsePreReleases sets the UsePreReleases flag. -// func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { -// reg.Lock() -// defer reg.Unlock() - -// reg.UsePreReleases = yes -// } - -// // AddResource adds a resource to the registry. Does _not_ select new version. -// func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { -// reg.Lock() -// defer reg.Unlock() - -// err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) -// return err -// } - -// func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { -// res, ok := reg.resources[identifier] -// if !ok { -// res = reg.newResource(identifier) -// reg.resources[identifier] = res -// } -// res.Index = index - -// return res.AddVersion(version, available, currentRelease, preRelease) -// } - -// // AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. -// func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { -// reg.Lock() -// defer reg.Unlock() - -// // add versions and their flags to registry -// var lastError error -// for identifier, version := range versions { -// lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) -// if lastError != nil { -// log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) -// } -// } - -// return lastError -// } - -// // SelectVersions selects new resource versions depending on the current registry state. -// func (reg *ResourceRegistry) SelectVersions() { -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// res.Lock() -// res.selectVersion() -// res.Unlock() -// } -// } - -// // GetSelectedVersions returns a list of the currently selected versions. -// func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// res.Lock() -// versions[res.Identifier] = res.SelectedVersion.VersionNumber -// res.Unlock() -// } - -// return -// } - -// // Purge deletes old updates, retaining a certain amount, specified by the keep -// // parameter. Will at least keep 2 updates per resource. -// func (reg *ResourceRegistry) Purge(keep int) { -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// res.Purge(keep) -// } -// } - -// // ResetResources removes all resources from the registry. -// func (reg *ResourceRegistry) ResetResources() { -// reg.Lock() -// defer reg.Unlock() - -// reg.resources = make(map[string]*Resource) -// } - -// // ResetIndexes removes all indexes from the registry. -// func (reg *ResourceRegistry) ResetIndexes() { -// reg.Lock() -// defer reg.Unlock() - -// reg.indexes = make([]*Index, 0, len(reg.indexes)) -// } - -// // Cleanup removes temporary files. -// func (reg *ResourceRegistry) Cleanup() error { -// // delete download tmp dir -// return os.RemoveAll(reg.tmpDir.Path) -// } diff --git a/base/updater/registry_test.go b/base/updater/registry_test.go deleted file mode 100644 index a8978f68..00000000 --- a/base/updater/registry_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package updater - -import ( - "os" - "testing" - - "github.com/safing/portmaster/base/utils" -) - -var registry *ResourceRegistry - -func TestMain(m *testing.M) { - // setup - tmpDir, err := os.MkdirTemp("", "ci-portmaster-") - if err != nil { - panic(err) - } - registry = &ResourceRegistry{ - UsePreReleases: true, - DevMode: true, - Online: true, - } - err = registry.Initialize(utils.NewDirStructure(tmpDir, 0o0777)) - if err != nil { - panic(err) - } - - // run - // call flag.Parse() here if TestMain uses flags - ret := m.Run() - - // teardown - _ = os.RemoveAll(tmpDir) - os.Exit(ret) -} diff --git a/base/updater/resource.go b/base/updater/resource.go deleted file mode 100644 index 9180d0c9..00000000 --- a/base/updater/resource.go +++ /dev/null @@ -1,582 +0,0 @@ -package updater - -// import ( -// "errors" -// "io/fs" -// "os" -// "path/filepath" -// "sort" -// "strings" -// "sync" - -// semver "github.com/hashicorp/go-version" - -// "github.com/safing/jess/filesig" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// var devVersion *semver.Version - -// func init() { -// var err error -// devVersion, err = semver.NewVersion("0") -// if err != nil { -// panic(err) -// } -// } - -// Resource represents a resource (via an identifier) and multiple file versions. -// type Resource struct { -// sync.Mutex -// registry *ResourceRegistry -// notifier *notifier - -// // Identifier is the unique identifier for that resource. -// // It forms a file path using a forward-slash as the -// // path separator. -// Identifier string - -// // Versions holds all available resource versions. -// Versions []*ResourceVersion - -// // ActiveVersion is the last version of the resource -// // that someone requested using GetFile(). -// ActiveVersion *ResourceVersion - -// // SelectedVersion is newest, selectable version of -// // that resource that is available. A version -// // is selectable if it's not blacklisted by the user. -// // Note that it's not guaranteed that the selected version -// // is available locally. In that case, GetFile will attempt -// // to download the latest version from the updates servers -// // specified in the resource registry. -// SelectedVersion *ResourceVersion - -// // VerificationOptions holds the verification options for this resource. -// VerificationOptions *VerificationOptions - -// // Index holds a reference to the index this resource was last defined in. -// // Will be nil if resource was only found on disk. -// Index *Index -// } - -// ResourceVersion represents a single version of a resource. -// type ResourceVersion struct { -// resource *Resource - -// // VersionNumber is the string representation of the resource -// // version. -// VersionNumber string -// semVer *semver.Version - -// // Available indicates if this version is available locally. -// Available bool - -// // SigAvailable indicates if the signature of this version is available locally. -// SigAvailable bool - -// // CurrentRelease indicates that this is the current release that should be -// // selected, if possible. -// CurrentRelease bool - -// // PreRelease indicates that this version is pre-release. -// PreRelease bool - -// // Blacklisted may be set to true if this version should -// // be skipped and not used. This is useful if the version -// // is known to be broken. -// Blacklisted bool -// } - -// func (rv *ResourceVersion) String() string { -// return rv.VersionNumber -// } - -// // SemVer returns the semantic version of the resource. -// func (rv *ResourceVersion) SemVer() *semver.Version { -// return rv.semVer -// } - -// EqualsVersion normalizes the given version and checks equality with semver. -// func (rv *ResourceVersion) EqualsVersion(version string) bool { -// cmpSemVer, err := semver.NewVersion(version) -// if err != nil { -// return false -// } - -// return rv.semVer.Equal(cmpSemVer) -// } - -// // isSelectable returns true if the version represented by rv is selectable. -// // A version is selectable if it's not blacklisted and either already locally -// // available or ready to be downloaded. -// func (rv *ResourceVersion) isSelectable() bool { -// switch { -// case rv.Blacklisted: -// // Should not be used. -// return false -// case rv.Available: -// // Is available locally, use! -// return true -// case !rv.resource.registry.Online: -// // Cannot download, because registry is set to offline. -// return false -// case rv.resource.Index == nil: -// // Cannot download, because resource is not part of an index. -// return false -// case !rv.resource.Index.AutoDownload: -// // Cannot download, because index may not automatically download. -// return false -// default: -// // Is not available locally, but we are allowed to download it on request! -// return true -// } -// } - -// isBetaVersionNumber checks if rv is marked as a beta version by checking -// the version string. It does not honor the BetaRelease field of rv! -// func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused -// // "b" suffix check if for backwards compatibility -// // new versions should use the pre-release suffix as -// // declared by https://semver.org -// // i.e. 1.2.3-beta -// switch rv.semVer.Prerelease() { -// case "b", "beta": -// return true -// default: -// return false -// } -// } - -// Export makes a copy of the resource with only the exposed information. -// Attributes are copied and safe to access. -// Any ResourceVersion must not be modified. -// func (res *Resource) Export() *Resource { -// res.Lock() -// defer res.Unlock() - -// // Copy attibutes. -// export := &Resource{ -// Identifier: res.Identifier, -// Versions: make([]*ResourceVersion, len(res.Versions)), -// ActiveVersion: res.ActiveVersion, -// SelectedVersion: res.SelectedVersion, -// } -// // Copy Versions slice. -// copy(export.Versions, res.Versions) - -// return export -// } - -// // Len is the number of elements in the collection. -// // It implements sort.Interface for ResourceVersion. -// func (res *Resource) Len() int { -// return len(res.Versions) -// } - -// // Less reports whether the element with index i should -// // sort before the element with index j. -// // It implements sort.Interface for ResourceVersions. -// func (res *Resource) Less(i, j int) bool { -// return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer) -// } - -// // Swap swaps the elements with indexes i and j. -// // It implements sort.Interface for ResourceVersions. -// func (res *Resource) Swap(i, j int) { -// res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i] -// } - -// // available returns whether any version of the resource is available. -// func (res *Resource) available() bool { -// for _, rv := range res.Versions { -// if rv.Available { -// return true -// } -// } -// return false -// } - -// // inUse returns true if the resource is currently in use. -// func (res *Resource) inUse() bool { -// return res.ActiveVersion != nil -// } - -// // AnyVersionAvailable returns true if any version of -// // res is locally available. -// func (res *Resource) AnyVersionAvailable() bool { -// res.Lock() -// defer res.Unlock() - -// return res.available() -// } - -// func (reg *ResourceRegistry) newResource(identifier string) *Resource { -// return &Resource{ -// registry: reg, -// Identifier: identifier, -// Versions: make([]*ResourceVersion, 0, 1), -// VerificationOptions: reg.GetVerificationOptions(identifier), -// } -// } - -// // AddVersion adds a resource version to a resource. -// func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error { -// res.Lock() -// defer res.Unlock() - -// // reset current release flags -// if currentRelease { -// for _, rv := range res.Versions { -// rv.CurrentRelease = false -// } -// } - -// var rv *ResourceVersion -// // check for existing version -// for _, possibleMatch := range res.Versions { -// if possibleMatch.VersionNumber == version { -// rv = possibleMatch -// break -// } -// } - -// // create new version if none found -// if rv == nil { -// // parse to semver -// sv, err := semver.NewVersion(version) -// if err != nil { -// return err -// } - -// rv = &ResourceVersion{ -// resource: res, -// VersionNumber: sv.String(), // Use normalized version. -// semVer: sv, -// } -// res.Versions = append(res.Versions, rv) -// } - -// // set flags -// if available { -// rv.Available = true - -// // If available and signatures are enabled for this resource, check if the -// // signature is available. -// if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) { -// rv.SigAvailable = true -// } -// } -// if currentRelease { -// rv.CurrentRelease = true -// } -// if preRelease || rv.semVer.Prerelease() != "" { -// rv.PreRelease = true -// } - -// return nil -// } - -// // GetFile returns the selected version as a *File. -// func (res *Resource) GetFile() *File { -// res.Lock() -// defer res.Unlock() - -// // check for notifier -// if res.notifier == nil { -// // create new notifier -// res.notifier = newNotifier() -// } - -// // check if version is selected -// if res.SelectedVersion == nil { -// res.selectVersion() -// } - -// // create file -// return &File{ -// resource: res, -// version: res.SelectedVersion, -// notifier: res.notifier, -// versionedPath: res.SelectedVersion.versionedPath(), -// storagePath: res.SelectedVersion.storagePath(), -// } -// } - -// //nolint:gocognit // function already kept as simple as possible -// func (res *Resource) selectVersion() { -// sort.Sort(res) - -// // export after we finish -// var fallback bool -// defer func() { -// if fallback { -// log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier) -// } else { -// log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier) -// } - -// if res.inUse() && -// res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version -// res.notifier != nil { - -// res.notifier.markAsUpgradeable() -// res.notifier = nil - -// log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber) -// } -// }() - -// if len(res.Versions) == 0 { -// // TODO: find better way to deal with an empty version slice (which should not happen) -// res.SelectedVersion = nil -// return -// } - -// // Target selection - -// // 1) Dev release if dev mode is active and ignore blacklisting -// if res.registry.DevMode { -// // Get last version, as this will be v0.0.0, if available. -// rv := res.Versions[len(res.Versions)-1] -// // Check if it's v0.0.0. -// if rv.semVer.Equal(devVersion) && rv.Available { -// res.SelectedVersion = rv -// return -// } -// } - -// // 2) Find the current release. This may be also be a pre-release. -// for _, rv := range res.Versions { -// if rv.CurrentRelease { -// if rv.isSelectable() { -// res.SelectedVersion = rv -// return -// } -// // There can only be once current release, -// // so we can abort after finding one. -// break -// } -// } - -// // 3) If UsePreReleases is set, find any newest version. -// if res.registry.UsePreReleases { -// for _, rv := range res.Versions { -// if rv.isSelectable() { -// res.SelectedVersion = rv -// return -// } -// } -// } - -// // 4) Find the newest stable version. -// for _, rv := range res.Versions { -// if !rv.PreRelease && rv.isSelectable() { -// res.SelectedVersion = rv -// return -// } -// } - -// // 5) Default to newest. -// res.SelectedVersion = res.Versions[0] -// fallback = true -// } - -// // Blacklist blacklists the specified version and selects a new version. -// func (res *Resource) Blacklist(version string) error { -// res.Lock() -// defer res.Unlock() - -// // count available and valid versions -// valid := 0 -// for _, rv := range res.Versions { -// if rv.semVer.Equal(devVersion) { -// continue // ignore dev versions -// } -// if !rv.Blacklisted { -// valid++ -// } -// } -// if valid <= 1 { -// return errors.New("cannot blacklist last version") // last one, cannot blacklist! -// } - -// // find version and blacklist -// for _, rv := range res.Versions { -// if rv.VersionNumber == version { -// // blacklist and update -// rv.Blacklisted = true -// res.selectVersion() -// return nil -// } -// } - -// return errors.New("could not find version") -// } - -// // Purge deletes old updates, retaining a certain amount, specified by -// // the keep parameter. Purge will always keep at least 2 versions so -// // specifying a smaller keep value will have no effect. -// func (res *Resource) Purge(keepExtra int) { //nolint:gocognit -// res.Lock() -// defer res.Unlock() - -// // If there is any blacklisted version within the resource, pause purging. -// // In this case we may need extra available versions beyond what would be -// // available after purging. -// for _, rv := range res.Versions { -// if rv.Blacklisted { -// log.Debugf( -// "%s: pausing purging of resource %s, as it contains blacklisted items", -// res.registry.Name, -// rv.resource.Identifier, -// ) -// return -// } -// } - -// // Safeguard the amount of extra version to keep. -// if keepExtra < 2 { -// keepExtra = 2 -// } - -// // Search for purge boundary. -// var purgeBoundary int -// var skippedActiveVersion bool -// var skippedSelectedVersion bool -// var skippedStableVersion bool -// boundarySearch: -// for i, rv := range res.Versions { -// // Check if required versions are already skipped. -// switch { -// case !skippedActiveVersion && res.ActiveVersion != nil: -// // Skip versions until the active version, if it's set. -// case !skippedSelectedVersion && res.SelectedVersion != nil: -// // Skip versions until the selected version, if it's set. -// case !skippedStableVersion: -// // Skip versions until the stable version. -// default: -// // All required version skipped, set purge boundary. -// purgeBoundary = i + keepExtra -// break boundarySearch -// } - -// // Check if current instance is a required version. -// if rv == res.ActiveVersion { -// skippedActiveVersion = true -// } -// if rv == res.SelectedVersion { -// skippedSelectedVersion = true -// } -// if !rv.PreRelease { -// skippedStableVersion = true -// } -// } - -// // Check if there is anything to purge at all. -// if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) { -// return -// } - -// // Purge everything beyond the purge boundary. -// for _, rv := range res.Versions[purgeBoundary:] { -// // Only remove if resource file is actually available. -// if !rv.Available { -// continue -// } - -// // Remove resource file. -// storagePath := rv.storagePath() -// err := os.Remove(storagePath) -// if err != nil { -// if !errors.Is(err, fs.ErrNotExist) { -// log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) -// } -// } else { -// log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) -// } - -// // Remove resource signature file. -// err = os.Remove(rv.storageSigPath()) -// if err != nil { -// if !errors.Is(err, fs.ErrNotExist) { -// log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) -// } -// } else { -// log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) -// } - -// // Remove unpacked version of resource. -// ext := filepath.Ext(storagePath) -// if ext == "" { -// // Nothing to do if file does not have an extension. -// continue -// } -// unpackedPath := strings.TrimSuffix(storagePath, ext) - -// // Remove if it exists, or an error occurs on access. -// _, err = os.Stat(unpackedPath) -// if err == nil || !errors.Is(err, fs.ErrNotExist) { -// err = os.Remove(unpackedPath) -// if err != nil { -// log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) -// } else { -// log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) -// } -// } -// } - -// // remove entries of deleted files -// res.Versions = res.Versions[purgeBoundary:] -// } - -// // SigningMetadata returns the metadata to be included in signatures. -// func (rv *ResourceVersion) SigningMetadata() map[string]string { -// return map[string]string{ -// "id": rv.resource.Identifier, -// "version": rv.VersionNumber, -// } -// } - -// // GetFile returns the version as a *File. -// // It locks the resource for doing so. -// func (rv *ResourceVersion) GetFile() *File { -// rv.resource.Lock() -// defer rv.resource.Unlock() - -// // check for notifier -// if rv.resource.notifier == nil { -// // create new notifier -// rv.resource.notifier = newNotifier() -// } - -// // create file -// return &File{ -// resource: rv.resource, -// version: rv, -// notifier: rv.resource.notifier, -// versionedPath: rv.versionedPath(), -// storagePath: rv.storagePath(), -// } -// } - -// // versionedPath returns the versioned identifier. -// func (rv *ResourceVersion) versionedPath() string { -// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) -// } - -// // versionedSigPath returns the versioned identifier of the file signature. -// func (rv *ResourceVersion) versionedSigPath() string { -// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension -// } - -// // storagePath returns the absolute storage path. -// func (rv *ResourceVersion) storagePath() string { -// return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath())) -// } - -// // storageSigPath returns the absolute storage path of the file signature. -// func (rv *ResourceVersion) storageSigPath() string { -// return rv.storagePath() + filesig.Extension -// } diff --git a/base/updater/resource_test.go b/base/updater/resource_test.go deleted file mode 100644 index ceb51e9f..00000000 --- a/base/updater/resource_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package updater - -import ( - "fmt" - "testing" - - semver "github.com/hashicorp/go-version" - "github.com/stretchr/testify/assert" -) - -func TestVersionSelection(t *testing.T) { - t.Parallel() - - res := registry.newResource("test/a") - - err := res.AddVersion("1.2.2", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.3", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.4-beta", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.4-staging", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.5", false, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.6-beta", false, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("0", true, false, false) - if err != nil { - t.Fatal(err) - } - - registry.UsePreReleases = true - registry.DevMode = true - registry.Online = true - res.Index = &Index{AutoDownload: true} - - res.selectVersion() - if res.SelectedVersion.VersionNumber != "0.0.0" { - t.Errorf("selected version should be 0.0.0, not %s", res.SelectedVersion.VersionNumber) - } - - registry.DevMode = false - res.selectVersion() - if res.SelectedVersion.VersionNumber != "1.2.6-beta" { - t.Errorf("selected version should be 1.2.6-beta, not %s", res.SelectedVersion.VersionNumber) - } - - registry.UsePreReleases = false - res.selectVersion() - if res.SelectedVersion.VersionNumber != "1.2.5" { - t.Errorf("selected version should be 1.2.5, not %s", res.SelectedVersion.VersionNumber) - } - - registry.Online = false - res.selectVersion() - if res.SelectedVersion.VersionNumber != "1.2.3" { - t.Errorf("selected version should be 1.2.3, not %s", res.SelectedVersion.VersionNumber) - } - - f123 := res.GetFile() - f123.markActiveWithLocking() - - err = res.Blacklist("1.2.3") - if err != nil { - t.Fatal(err) - } - if res.SelectedVersion.VersionNumber != "1.2.2" { - t.Errorf("selected version should be 1.2.2, not %s", res.SelectedVersion.VersionNumber) - } - - if !f123.UpgradeAvailable() { - t.Error("upgrade should be available (flag)") - } - select { - case <-f123.WaitForAvailableUpgrade(): - default: - t.Error("upgrade should be available (chan)") - } - - t.Logf("resource: %+v", res) - for _, rv := range res.Versions { - t.Logf("version %s: %+v", rv.VersionNumber, rv) - } -} - -func TestVersionParsing(t *testing.T) { - t.Parallel() - - assert.Equal(t, "1.2.3", parseVersion("1.2.3")) - assert.Equal(t, "1.2.0", parseVersion("1.2.0")) - assert.Equal(t, "0.2.0", parseVersion("0.2.0")) - assert.Equal(t, "0.0.0", parseVersion("0")) - assert.Equal(t, "1.2.3-b", parseVersion("1.2.3-b")) - assert.Equal(t, "1.2.3-b", parseVersion("1.2.3b")) - assert.Equal(t, "1.2.3-beta", parseVersion("1.2.3-beta")) - assert.Equal(t, "1.2.3-beta", parseVersion("1.2.3beta")) - assert.Equal(t, "1.2.3", parseVersion("01.02.03")) -} - -func parseVersion(v string) string { - sv, err := semver.NewVersion(v) - if err != nil { - return fmt.Sprintf("failed to parse version: %s", err) - } - return sv.String() -} diff --git a/base/updater/signing.go b/base/updater/signing.go deleted file mode 100644 index 85f4eb6f..00000000 --- a/base/updater/signing.go +++ /dev/null @@ -1,49 +0,0 @@ -package updater - -// import ( -// "strings" - -// "github.com/safing/jess" -// ) - -// // VerificationOptions holds options for verification of files. -// type VerificationOptions struct { -// TrustStore jess.TrustStore -// DownloadPolicy SignaturePolicy -// DiskLoadPolicy SignaturePolicy -// } - -// // GetVerificationOptions returns the verification options for the given identifier. -// func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { -// if reg.Verification == nil { -// return nil -// } - -// var ( -// longestPrefix = -1 -// bestMatch *VerificationOptions -// ) -// for prefix, opts := range reg.Verification { -// if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { -// longestPrefix = len(prefix) -// bestMatch = opts -// } -// } - -// return bestMatch -// } - -// // SignaturePolicy defines behavior in case of errors. -// type SignaturePolicy uint8 - -// // Signature Policies. -// const ( -// // SignaturePolicyRequire fails on any error. -// SignaturePolicyRequire = iota - -// // SignaturePolicyWarn only warns on errors. -// SignaturePolicyWarn - -// // SignaturePolicyDisable only downloads signatures, but does not verify them. -// SignaturePolicyDisable -// ) diff --git a/base/updater/state.go b/base/updater/state.go deleted file mode 100644 index 8cbada6f..00000000 --- a/base/updater/state.go +++ /dev/null @@ -1,180 +0,0 @@ -package updater - -// import ( -// "sort" -// "sync" -// "time" - -// "github.com/safing/portmaster/base/utils" -// ) - -// // Registry States. -// const ( -// StateReady = "ready" // Default idle state. -// StateChecking = "checking" // Downloading indexes. -// StateDownloading = "downloading" // Downloading updates. -// StateFetching = "fetching" // Fetching a single file. -// ) - -// // RegistryState describes the registry state. -// type RegistryState struct { -// sync.Mutex -// reg *ResourceRegistry - -// // ID holds the ID of the state the registry is currently in. -// ID string - -// // Details holds further information about the current state. -// Details any - -// // Updates holds generic information about the current status of pending -// // and recently downloaded updates. -// Updates UpdateState - -// // operationLock locks the operation of any state changing operation. -// // This is separate from the registry lock, which locks access to the -// // registry struct. -// operationLock sync.Mutex -// } - -// // StateDownloadingDetails holds details of the downloading state. -// type StateDownloadingDetails struct { -// // Resources holds the resource IDs that are being downloaded. -// Resources []string - -// // FinishedUpTo holds the index of Resources that is currently being -// // downloaded. Previous resources have finished downloading. -// FinishedUpTo int -// } - -// // UpdateState holds generic information about the current status of pending -// // and recently downloaded updates. -// type UpdateState struct { -// // LastCheckAt holds the time of the last update check. -// LastCheckAt *time.Time -// // LastCheckError holds the error of the last check. -// LastCheckError error -// // PendingDownload holds the resources that are pending download. -// PendingDownload []string - -// // LastDownloadAt holds the time when resources were downloaded the last time. -// LastDownloadAt *time.Time -// // LastDownloadError holds the error of the last download. -// LastDownloadError error -// // LastDownload holds the resources that we downloaded the last time updates -// // were downloaded. -// LastDownload []string - -// // LastSuccessAt holds the time of the last successful update (check). -// LastSuccessAt *time.Time -// } - -// // GetState returns the current registry state. -// // The returned data must not be modified. -// func (reg *ResourceRegistry) GetState() RegistryState { -// reg.state.Lock() -// defer reg.state.Unlock() - -// return RegistryState{ -// ID: reg.state.ID, -// Details: reg.state.Details, -// Updates: reg.state.Updates, -// } -// } - -// // StartOperation starts an operation. -// func (s *RegistryState) StartOperation(id string) bool { -// defer s.notify() - -// s.operationLock.Lock() - -// s.Lock() -// defer s.Unlock() - -// s.ID = id -// return true -// } - -// // UpdateOperationDetails updates the details of an operation. -// // The supplied struct should be a copy and must not be changed after calling -// // this function. -// func (s *RegistryState) UpdateOperationDetails(details any) { -// defer s.notify() - -// s.Lock() -// defer s.Unlock() - -// s.Details = details -// } - -// // EndOperation ends an operation. -// func (s *RegistryState) EndOperation() { -// defer s.notify() -// defer s.operationLock.Unlock() - -// s.Lock() -// defer s.Unlock() - -// s.ID = StateReady -// s.Details = nil -// } - -// // ReportUpdateCheck reports an update check to the registry state. -// func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { -// defer s.notify() - -// sort.Strings(pendingDownload) - -// s.Lock() -// defer s.Unlock() - -// now := time.Now() -// s.Updates.LastCheckAt = &now -// s.Updates.LastCheckError = failed -// s.Updates.PendingDownload = pendingDownload - -// if failed == nil { -// s.Updates.LastSuccessAt = &now -// } -// } - -// // ReportDownloads reports downloaded updates to the registry state. -// func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { -// defer s.notify() - -// sort.Strings(downloaded) - -// s.Lock() -// defer s.Unlock() - -// now := time.Now() -// s.Updates.LastDownloadAt = &now -// s.Updates.LastDownloadError = failed -// s.Updates.LastDownload = downloaded - -// // Remove downloaded resources from the pending list. -// if len(s.Updates.PendingDownload) > 0 { -// newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) -// for _, pending := range s.Updates.PendingDownload { -// if !utils.StringInSlice(downloaded, pending) { -// newPendingDownload = append(newPendingDownload, pending) -// } -// } -// s.Updates.PendingDownload = newPendingDownload -// } - -// if failed == nil { -// s.Updates.LastSuccessAt = &now -// } -// } - -// func (s *RegistryState) notify() { -// switch { -// case s.reg == nil: -// return -// case s.reg.StateNotifyFunc == nil: -// return -// } - -// s.reg.StateNotifyFunc(s) -// } diff --git a/base/updater/storage.go b/base/updater/storage.go deleted file mode 100644 index 8cfe8444..00000000 --- a/base/updater/storage.go +++ /dev/null @@ -1,272 +0,0 @@ -package updater - -// import ( -// "context" -// "errors" -// "fmt" -// "io/fs" -// "net/http" -// "os" -// "path/filepath" -// "strings" - -// "github.com/safing/jess/filesig" -// "github.com/safing/jess/lhash" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// // ScanStorage scans root within the storage dir and adds found -// // resources to the registry. If an error occurred, it is logged -// // and the last error is returned. Everything that was found -// // despite errors is added to the registry anyway. Leave root -// // empty to scan the full storage dir. -// func (reg *ResourceRegistry) ScanStorage(root string) error { -// var lastError error - -// // prep root -// if root == "" { -// root = reg.storageDir.Path -// } else { -// var err error -// root, err = filepath.Abs(root) -// if err != nil { -// return err -// } -// if !strings.HasPrefix(root, reg.storageDir.Path) { -// return errors.New("supplied scan root path not within storage") -// } -// } - -// // walk fs -// _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { -// // skip tmp dir (including errors trying to read it) -// if strings.HasPrefix(path, reg.tmpDir.Path) { -// return filepath.SkipDir -// } - -// // handle walker error -// if err != nil { -// lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) -// log.Warning(lastError.Error()) -// return nil -// } - -// // Ignore file signatures. -// if strings.HasSuffix(path, filesig.Extension) { -// return nil -// } - -// // get relative path to storage -// relativePath, err := filepath.Rel(reg.storageDir.Path, path) -// if err != nil { -// lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) -// log.Warning(lastError.Error()) -// return nil -// } - -// // convert to identifier and version -// relativePath = filepath.ToSlash(relativePath) -// identifier, version, ok := GetIdentifierAndVersion(relativePath) -// if !ok { -// // file does not conform to format -// return nil -// } - -// // fully ignore directories that also have an identifier - these will be unpacked resources -// if info.IsDir() { -// return filepath.SkipDir -// } - -// // save -// err = reg.AddResource(identifier, version, nil, true, false, false) -// if err != nil { -// lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) -// log.Warning(lastError.Error()) -// } -// return nil -// }) - -// return lastError -// } - -// // LoadIndexes loads the current release indexes from disk -// // or will fetch a new version if not available and the -// // registry is marked as online. -// func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { -// var firstErr error -// client := &http.Client{} -// for _, idx := range reg.getIndexes() { -// err := reg.loadIndexFile(idx) -// if err == nil { -// log.Debugf("%s: loaded index %s", reg.Name, idx.Path) -// } else if reg.Online { -// // try to download the index file if a local disk version -// // does not exist or we don't have permission to read it. -// if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { -// err = reg.downloadIndex(ctx, client, idx) -// } -// } - -// if err != nil && firstErr == nil { -// firstErr = err -// } -// } - -// return firstErr -// } - -// // getIndexes returns a copy of the index. -// // The indexes itself are references. -// func (reg *ResourceRegistry) getIndexes() []*Index { -// reg.RLock() -// defer reg.RUnlock() - -// indexes := make([]*Index, len(reg.indexes)) -// copy(indexes, reg.indexes) -// return indexes -// } - -// func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { -// indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) -// indexData, err := os.ReadFile(indexPath) -// if err != nil { -// return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) -// } - -// // Verify signature, if enabled. -// if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { -// // Load and check signature. -// verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) -// if err != nil { -// switch verifOpts.DiskLoadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) -// } -// } - -// // Check if signature checksum matches the index data. -// if err == nil && !verifiedHash.Matches(indexData) { -// switch verifOpts.DiskLoadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("index file %s does not match signature", idx.Path) -// case SignaturePolicyWarn: -// log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) -// case SignaturePolicyDisable: -// log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) -// } -// } -// } - -// // Parse the index file. -// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) -// if err != nil { -// return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) -// } - -// // Update last seen release. -// idx.LastRelease = indexFile.Published - -// // Warn if there aren't any releases in the index. -// if len(indexFile.Releases) == 0 { -// log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) -// return nil -// } - -// // Add index releases to available resources. -// err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) -// if err != nil { -// log.Warningf("%s: failed to add resource: %s", reg.Name, err) -// } -// return nil -// } - -// func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { -// // Load signature file. -// sigFileData, err := os.ReadFile(sigFilePath) -// if err != nil { -// return nil, nil, fmt.Errorf("failed to read signature file: %w", err) -// } - -// // Extract all signatures. -// sigs, err := filesig.ParseSigFile(sigFileData) -// switch { -// case len(sigs) == 0 && err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// case len(sigs) == 0: -// return nil, nil, errors.New("no signatures found in signature file") -// case err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// } - -// // Verify all signatures. -// var verifiedHash *lhash.LabeledHash -// for _, sig := range sigs { -// fd, err := filesig.VerifyFileData( -// sig, -// nil, -// verifOpts.TrustStore, -// ) -// if err != nil { -// return nil, sigFileData, err -// } - -// // Save or check verified hash. -// if verifiedHash == nil { -// verifiedHash = fd.FileHash() -// } else if !fd.FileHash().Equal(verifiedHash) { -// // Return an error if two valid hashes mismatch. -// // For simplicity, all hash algorithms must be the same for now. -// return nil, sigFileData, errors.New("file hashes from different signatures do not match") -// } -// } - -// return verifiedHash, sigFileData, nil -// } - -// // CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. -// func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { -// err := os.RemoveAll(symlinkRoot.Path) -// if err != nil { -// return fmt.Errorf("failed to wipe symlink root: %w", err) -// } - -// err = symlinkRoot.Ensure() -// if err != nil { -// return fmt.Errorf("failed to create symlink root: %w", err) -// } - -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// if res.SelectedVersion == nil { -// return fmt.Errorf("no selected version available for %s", res.Identifier) -// } - -// targetPath := res.SelectedVersion.storagePath() -// linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) -// linkPathDir := filepath.Dir(linkPath) - -// err = symlinkRoot.EnsureAbsPath(linkPathDir) -// if err != nil { -// return fmt.Errorf("failed to create dir for link: %w", err) -// } - -// relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) -// if err != nil { -// return fmt.Errorf("failed to get relative target path: %w", err) -// } - -// err = os.Symlink(relativeTargetPath, linkPath) -// if err != nil { -// return fmt.Errorf("failed to link %s: %w", res.Identifier, err) -// } -// } - -// return nil -// } diff --git a/base/updater/storage_test.go b/base/updater/storage_test.go deleted file mode 100644 index 2e4122fa..00000000 --- a/base/updater/storage_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package updater - -/* -func testLoadLatestScope(t *testing.T, basePath, filePath, expectedIdentifier, expectedVersion string) { - fullPath := filepath.Join(basePath, filePath) - - // create dir - dirPath := filepath.Dir(fullPath) - err := os.MkdirAll(dirPath, 0755) - if err != nil { - t.Fatalf("could not create test dir: %s\n", err) - return - } - - // touch file - err = os.WriteFile(fullPath, []byte{}, 0644) - if err != nil { - t.Fatalf("could not create test file: %s\n", err) - return - } - - // run loadLatestScope - latest, err := ScanForLatest(basePath, true) - if err != nil { - t.Errorf("could not update latest: %s\n", err) - return - } - for key, val := range latest { - localUpdates[key] = val - } - - // test result - version, ok := localUpdates[expectedIdentifier] - if !ok { - t.Errorf("identifier %s not in map", expectedIdentifier) - t.Errorf("current map: %v", localUpdates) - } - if version != expectedVersion { - t.Errorf("unexpected version for %s: %s", filePath, version) - } -} - -func TestLoadLatestScope(t *testing.T) { - - updatesLock.Lock() - defer updatesLock.Unlock() - - tmpDir, err := os.MkdirTemp("", "testing_") - if err != nil { - t.Fatalf("could not create test dir: %s\n", err) - return - } - defer os.RemoveAll(tmpDir) - - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "1.2.3") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4b.zip", "all/ui/assets.zip", "1.2.4b") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-5.zip", "all/ui/assets.zip", "1.2.5") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "1.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v2-3-4.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "1.2.3") - testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v2-1-1", "os_platform/portmaster/portmaster", "2.1.1") - testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "2.1.1") - -} -*/ diff --git a/base/updater/unpacking.go b/base/updater/unpacking.go deleted file mode 100644 index 5b157695..00000000 --- a/base/updater/unpacking.go +++ /dev/null @@ -1,195 +0,0 @@ -package updater - -// import ( -// "archive/zip" -// "compress/gzip" -// "errors" -// "fmt" -// "io" -// "io/fs" -// "os" -// "path" -// "path/filepath" -// "strings" - -// "github.com/hashicorp/go-multierror" - -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// // MaxUnpackSize specifies the maximum size that will be unpacked. -// const MaxUnpackSize = 1000000000 // 1GB - -// // UnpackGZIP unpacks a GZIP compressed reader r -// // and returns a new reader. It's suitable to be -// // used with registry.GetPackedFile. -// func UnpackGZIP(r io.Reader) (io.Reader, error) { -// return gzip.NewReader(r) -// } - -// // UnpackResources unpacks all resources defined in the AutoUnpack list. -// func (reg *ResourceRegistry) UnpackResources() error { -// reg.RLock() -// defer reg.RUnlock() - -// var multierr *multierror.Error -// for _, res := range reg.resources { -// if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { -// err := res.UnpackArchive() -// if err != nil { -// multierr = multierror.Append( -// multierr, -// fmt.Errorf("%s: %w", res.Identifier, err), -// ) -// } -// } -// } - -// return multierr.ErrorOrNil() -// } - -// const ( -// zipSuffix = ".zip" -// ) - -// // UnpackArchive unpacks the archive the resource refers to. The contents are -// // unpacked into a directory with the same name as the file, excluding the -// // suffix. If the destination folder already exists, it is assumed that the -// // contents have already been correctly unpacked. -// func (res *Resource) UnpackArchive() error { -// res.Lock() -// defer res.Unlock() - -// // Only unpack selected versions. -// if res.SelectedVersion == nil { -// return nil -// } - -// switch { -// case strings.HasSuffix(res.Identifier, zipSuffix): -// return res.unpackZipArchive() -// default: -// return fmt.Errorf("unsupported file type for unpacking") -// } -// } - -// func (res *Resource) unpackZipArchive() error { -// // Get file and directory paths. -// archiveFile := res.SelectedVersion.storagePath() -// destDir := strings.TrimSuffix(archiveFile, zipSuffix) -// tmpDir := filepath.Join( -// res.registry.tmpDir.Path, -// filepath.FromSlash(strings.TrimSuffix( -// path.Base(res.SelectedVersion.versionedPath()), -// zipSuffix, -// )), -// ) - -// // Check status of destination. -// dstStat, err := os.Stat(destDir) -// switch { -// case errors.Is(err, fs.ErrNotExist): -// // The destination does not exist, continue with unpacking. -// case err != nil: -// return fmt.Errorf("cannot access destination for unpacking: %w", err) -// case !dstStat.IsDir(): -// return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) -// default: -// // Archive already seems to be unpacked. -// return nil -// } - -// // Create the tmp directory for unpacking. -// err = res.registry.tmpDir.EnsureAbsPath(tmpDir) -// if err != nil { -// return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) -// } - -// // Defer clean up of directories. -// defer func() { -// // Always clean up the tmp dir. -// _ = os.RemoveAll(tmpDir) -// // Cleanup the destination in case of an error. -// if err != nil { -// _ = os.RemoveAll(destDir) -// } -// }() - -// // Open the archive for reading. -// var archiveReader *zip.ReadCloser -// archiveReader, err = zip.OpenReader(archiveFile) -// if err != nil { -// return fmt.Errorf("failed to open zip reader: %w", err) -// } -// defer func() { -// _ = archiveReader.Close() -// }() - -// // Save all files to the tmp dir. -// for _, file := range archiveReader.File { -// err = copyFromZipArchive( -// file, -// filepath.Join(tmpDir, filepath.FromSlash(file.Name)), -// ) -// if err != nil { -// return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) -// } -// } - -// // Make the final move. -// err = os.Rename(tmpDir, destDir) -// if err != nil { -// return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) -// } - -// // Fix permissions on the destination dir. -// err = res.registry.storageDir.EnsureAbsPath(destDir) -// if err != nil { -// return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) -// } - -// log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) -// return nil -// } - -// func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { -// // If file is a directory, create it and continue. -// if archiveFile.FileInfo().IsDir() { -// err := os.Mkdir(dstPath, archiveFile.Mode()) -// if err != nil { -// return fmt.Errorf("failed to create directory %s: %w", dstPath, err) -// } -// return nil -// } - -// // Open archived file for reading. -// fileReader, err := archiveFile.Open() -// if err != nil { -// return fmt.Errorf("failed to open file in archive: %w", err) -// } -// defer func() { -// _ = fileReader.Close() -// }() - -// // Open destination file for writing. -// dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) -// if err != nil { -// return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) -// } -// defer func() { -// _ = dstFile.Close() -// }() - -// // Copy full file from archive to dst. -// if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { -// // EOF is expected here as the archive is likely smaller -// // thane MaxUnpackSize -// if errors.Is(err, io.EOF) { -// return nil -// } -// return err -// } - -// return nil -// } diff --git a/base/updater/updating.go b/base/updater/updating.go deleted file mode 100644 index 3c76304a..00000000 --- a/base/updater/updating.go +++ /dev/null @@ -1,359 +0,0 @@ -package updater - -// import ( -// "context" -// "fmt" -// "net/http" -// "os" -// "path" -// "path/filepath" -// "strings" - -// "golang.org/x/exp/slices" - -// "github.com/safing/jess/filesig" -// "github.com/safing/jess/lhash" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// // UpdateIndexes downloads all indexes. An error is only returned when all -// // indexes fail to update. -// func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { -// var lastErr error -// var anySuccess bool - -// // Start registry operation. -// reg.state.StartOperation(StateChecking) -// defer reg.state.EndOperation() - -// client := &http.Client{} -// for _, idx := range reg.getIndexes() { -// if err := reg.downloadIndex(ctx, client, idx); err != nil { -// lastErr = err -// log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) -// } else { -// anySuccess = true -// } -// } - -// // If all indexes failed to update, fail. -// if !anySuccess { -// err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) -// reg.state.ReportUpdateCheck(nil, err) -// return err -// } - -// // Get pending resources and update status. -// pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) -// reg.state.ReportUpdateCheck( -// humanInfoFromResourceVersions(pendingResourceVersions), -// nil, -// ) - -// return nil -// } - -// func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { -// var ( -// // Index. -// indexErr error -// indexData []byte -// downloadURL string - -// // Signature. -// sigErr error -// verifiedHash *lhash.LabeledHash -// sigFileData []byte -// verifOpts = reg.GetVerificationOptions(idx.Path) -// ) - -// // Upgrade to v2 index if verification is enabled. -// downloadIndexPath := idx.Path -// if verifOpts != nil { -// downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension -// } - -// // Download new index and signature. -// for tries := range 3 { -// // Index and signature need to be fetched together, so that they are -// // fetched from the same source. One source should always have a matching -// // index and signature. Backup sources may be behind a little. -// // If the signature verification fails, another source should be tried. - -// // Get index data. -// indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) -// if indexErr != nil { -// log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) -// continue -// } - -// // Get signature and verify it. -// if verifOpts != nil { -// verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( -// ctx, client, -// verifOpts, downloadIndexPath+filesig.Extension, nil, -// tries, -// ) -// if sigErr != nil { -// log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) -// continue -// } - -// // Check if the index matches the verified hash. -// if verifiedHash.Matches(indexData) { -// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) -// } else { -// sigErr = ErrIndexChecksumMismatch -// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) -// continue -// } -// } - -// break -// } -// if indexErr != nil { -// return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) -// } -// if sigErr != nil { -// return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) -// } - -// // Parse the index file. -// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) -// if err != nil { -// return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) -// } - -// // Add index data to registry. -// if len(indexFile.Releases) > 0 { -// // Check if all resources are within the indexes' authority. -// authoritativePath := path.Dir(idx.Path) + "/" -// if authoritativePath == "./" { -// // Fix path for indexes at the storage root. -// authoritativePath = "" -// } -// cleanedData := make(map[string]string, len(indexFile.Releases)) -// for key, version := range indexFile.Releases { -// if strings.HasPrefix(key, authoritativePath) { -// cleanedData[key] = version -// } else { -// log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) -// } -// } - -// // add resources to registry -// err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) -// if err != nil { -// log.Warningf("%s: failed to add resources: %s", reg.Name, err) -// } -// } else { -// log.Debugf("%s: index %s is empty", reg.Name, idx.Path) -// } - -// // Check if dest dir exists. -// indexDir := filepath.FromSlash(path.Dir(idx.Path)) -// err = reg.storageDir.EnsureRelPath(indexDir) -// if err != nil { -// log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) -// } - -// // Index files must be readable by portmaster-staert with user permissions in order to load the index. -// err = os.WriteFile( //nolint:gosec -// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), -// indexData, 0o0644, -// ) -// if err != nil { -// log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) -// } - -// // Write signature file, if we have one. -// if len(sigFileData) > 0 { -// err = os.WriteFile( //nolint:gosec -// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), -// sigFileData, 0o0644, -// ) -// if err != nil { -// log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) -// } -// } - -// log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) -// return nil -// } - -// // DownloadUpdates checks if updates are available and downloads updates of used components. -// func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { -// // Start registry operation. -// reg.state.StartOperation(StateDownloading) -// defer reg.state.EndOperation() - -// // Get pending updates. -// toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) -// downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) -// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ -// Resources: downloadDetailsResources, -// }) - -// // nothing to update -// if len(toUpdate) == 0 && len(missingSigs) == 0 { -// log.Infof("%s: everything up to date", reg.Name) -// return nil -// } - -// // check download dir -// if err := reg.tmpDir.Ensure(); err != nil { -// return fmt.Errorf("could not prepare tmp directory for download: %w", err) -// } - -// // download updates -// log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) -// client := &http.Client{} -// var reportError error - -// for i, rv := range toUpdate { -// log.Infof( -// "%s: downloading update [%d/%d]: %s version %s", -// reg.Name, -// i+1, len(toUpdate), -// rv.resource.Identifier, rv.VersionNumber, -// ) -// var err error -// for tries := range 3 { -// err = reg.fetchFile(ctx, client, rv, tries) -// if err == nil { -// // Update resource version state. -// rv.resource.Lock() -// rv.Available = true -// if rv.resource.VerificationOptions != nil { -// rv.SigAvailable = true -// } -// rv.resource.Unlock() - -// break -// } -// } -// if err != nil { -// reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) -// log.Warningf("%s: %s", reg.Name, reportError) -// } - -// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ -// Resources: downloadDetailsResources, -// FinishedUpTo: i + 1, -// }) -// } - -// if len(missingSigs) > 0 { -// log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) - -// for _, rv := range missingSigs { -// var err error -// for tries := range 3 { -// err = reg.fetchMissingSig(ctx, client, rv, tries) -// if err == nil { -// // Update resource version state. -// rv.resource.Lock() -// rv.SigAvailable = true -// rv.resource.Unlock() - -// break -// } -// } -// if err != nil { -// reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) -// log.Warningf("%s: %s", reg.Name, reportError) -// } -// } -// } - -// reg.state.ReportDownloads( -// downloadDetailsResources, -// reportError, -// ) -// log.Infof("%s: finished downloading updates", reg.Name) - -// return nil -// } - -// // DownloadUpdates checks if updates are available and downloads updates of used components. - -// // GetPendingDownloads returns the list of pending downloads. -// // If manual is set, indexes with AutoDownload=false will be checked. -// // If auto is set, indexes with AutoDownload=true will be checked. -// func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { -// reg.RLock() -// defer reg.RUnlock() - -// // create list of downloads -// var toUpdate []*ResourceVersion -// var missingSigs []*ResourceVersion - -// for _, res := range reg.resources { -// func() { -// res.Lock() -// defer res.Unlock() - -// // Skip resources without index or indexes that should not be reported -// // according to parameters. -// switch { -// case res.Index == nil: -// // Cannot download if resource is not part of an index. -// return -// case manual && !res.Index.AutoDownload: -// // Manual update report and index is not auto-download. -// case auto && res.Index.AutoDownload: -// // Auto update report and index is auto-download. -// default: -// // Resource should not be reported. -// return -// } - -// // Skip resources we don't need. -// switch { -// case res.inUse(): -// // Update if resource is in use. -// case res.available(): -// // Update if resource is available locally, ie. was used in the past. -// case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): -// // Update is set as mandatory. -// default: -// // Resource does not need to be updated. -// return -// } - -// // Go through all versions until we find versions that need updating. -// for _, rv := range res.Versions { -// switch { -// case !rv.CurrentRelease: -// // We are not interested in older releases. -// case !rv.Available: -// // File not available locally, download! -// toUpdate = append(toUpdate, rv) -// case !rv.SigAvailable && res.VerificationOptions != nil: -// // File signature is not available and verification is enabled, download signature! -// missingSigs = append(missingSigs, rv) -// } -// } -// }() -// } - -// slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { -// return strings.Compare(a.resource.Identifier, b.resource.Identifier) -// }) -// slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { -// return strings.Compare(a.resource.Identifier, b.resource.Identifier) -// }) - -// return toUpdate, missingSigs -// } - -// func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { -// identifiers := make([]string, len(resourceVersions)) - -// for i, rv := range resourceVersions { -// identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) -// } - -// return identifiers -// } diff --git a/service/updates/registry.go b/service/updates/registry.go index 35dc67be..4a797947 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -6,7 +6,6 @@ import ( "io" "os" "path/filepath" - "runtime" semver "github.com/hashicorp/go-version" "github.com/safing/portmaster/base/log" @@ -112,19 +111,6 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { } else { log.Debugf("updates: %s moved", artifact.Filename) } - - // Special case for linux. - // When installed the portmaster ui path is `/usr/bin/portmaster`. During update the ui will be placed in `/usr/lib/portmaster/portmaster` - // After an update the original binary should be deleted and replaced by symlink - // `/usr/bin/portmaster` -> `/usr/lib/portmaster/portmaster` - if runtime.GOOS == "linux" && artifact.Filename == "portmaster" && artifact.Platform == currentPlatform { - err = r.makeSymlinkForUI() - if err != nil { - log.Errorf("failed to create symlink for the ui: %s", err) - } else { - log.Infof("updates: ui symlink successfully created") - } - } } log.Infof("updates: update complete") @@ -212,16 +198,6 @@ func (r *Registry) CleanOldFiles() error { return nil } -func (r *Registry) makeSymlinkForUI() error { - portmasterBinPath := "/usr/bin/portmaster" - _ = os.Remove(portmasterBinPath) - err := os.Symlink(filepath.Join(r.dir, "portmaster"), portmasterBinPath) - if err != nil { - return fmt.Errorf("failed to create symlink: %w", err) - } - return nil -} - type File struct { id string path string From c9631daa3eec8c0ab338a4751d6fe4a23c142652 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 26 Sep 2024 10:43:47 +0300 Subject: [PATCH 19/62] [WIP] Add update notifications --- service/updates/module.go | 51 ++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/service/updates/module.go b/service/updates/module.go index 16858836..625fe395 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -15,20 +15,11 @@ import ( const ( updateTaskRepeatDuration = 1 * time.Hour updateAvailableNotificationID = "updates:update-available" + updateFailedNotificationID = "updates:update-failed" // ResourceUpdateEvent is emitted every time the // updater successfully performed a resource update. - // ResourceUpdateEvent is emitted even if no new - // versions are available. Subscribers are expected - // to check if new versions of their resources are - // available by checking File.UpgradeAvailable(). ResourceUpdateEvent = "resource update" - - // VersionUpdateEvent is emitted every time a new - // version of a monitored resource is selected. - // During module initialization VersionUpdateEvent - // is also emitted. - VersionUpdateEvent = "active version update" ) // UserAgent is an HTTP User-Agent that is used to add @@ -57,7 +48,6 @@ type Updates struct { upgraderWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] - EventVersionsUpdated *mgr.EventMgr[struct{}] registry Registry downloader Downloader @@ -76,7 +66,6 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { states: m.NewStateMgr(), EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), - EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), autoApply: index.AutoApply, needsRestart: index.NeedsRestart, @@ -84,9 +73,8 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { instance: instance, } - // Events - module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil) - module.updateCheckWorkerMgr.Repeat(updateTaskRepeatDuration) + // Workers + module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil).Repeat(updateTaskRepeatDuration) module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", module.applyUpdates, nil) var err error @@ -101,17 +89,18 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { } func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { + // Download the index file. err := u.downloader.downloadIndexFile(wc.Ctx()) if err != nil { return fmt.Errorf("failed to download index file: %w", err) } - - defer u.EventResourcesUpdated.Submit(struct{}{}) - + // Check if there is a new version. if u.downloader.version.LessThanOrEqual(u.registry.version) { log.Infof("updates: check compete: no new updates") return nil } + + // Download the new version. downloadBundle := u.downloader.bundle log.Infof("updates: check complete: downloading new version: %s %s", downloadBundle.Name, downloadBundle.Version) err = u.downloader.copyMatchingFilesFromCurrent(u.registry.files) @@ -123,9 +112,11 @@ func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { log.Errorf("updates: failed to download update: %s", err) } else { if u.autoApply { + // Trigger upgrade. u.upgraderWorkerMgr.Go() } else { - notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{ + // Notify the user with option to trigger upgrade. + notifications.NotifyPrompt(updateAvailableNotificationID, "New update is available.", fmt.Sprintf("%s %s", downloadBundle.Name, downloadBundle.Version), notifications.Action{ ID: "apply", Text: "Apply", Type: notifications.ActionTypeWebhook, @@ -142,16 +133,26 @@ func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error { currentBundle := u.registry.bundle downloadBundle := u.downloader.bundle + if u.downloader.version.LessThanOrEqual(u.registry.version) { + // No new version, silently return. + return nil + } + log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) err := u.registry.performRecoverableUpgrade(u.downloader.dir, u.downloader.indexFile) if err != nil { - // TODO(vladimir): Send notification to UI - log.Errorf("updates: failed to apply updates: %s", err) - } else if u.needsRestart { - // TODO(vladimir): Prompt user to restart? - u.instance.Restart() + // Notify the user that update failed. + notifications.NotifyPrompt(updateFailedNotificationID, "Failed to apply update.", err.Error()) + return fmt.Errorf("updates: failed to apply updates: %w", err) + } + + if u.needsRestart { + // Perform restart. + u.instance.Restart() + } else { + // Update completed and no restart was needed. Submit an event. + u.EventResourcesUpdated.Submit(struct{}{}) } - u.EventResourcesUpdated.Submit(struct{}{}) return nil } From 61babe2822d762b0d733e2975198fa21087463ad Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 26 Sep 2024 13:51:42 +0300 Subject: [PATCH 20/62] [WIP] Add restart command to instance --- cmds/portmaster-core/main.go | 5 +- cmds/portmaster-core/main_linux.go | 22 +++++++ cmds/portmaster-core/main_windows.go | 11 ++++ service/config.go | 7 ++- service/core/api.go | 5 +- service/core/core.go | 1 + service/instance.go | 30 +++++++++- service/updates/downloader.go | 1 + service/updates/index.go | 1 - service/updates/main.go | 1 - service/updates/module.go | 2 +- service/updates/registry.go | 1 + service/updates/restart.go | 85 ---------------------------- spn/captain/hooks.go | 5 +- spn/captain/intel.go | 2 +- 15 files changed, 81 insertions(+), 98 deletions(-) delete mode 100644 service/updates/index.go delete mode 100644 service/updates/main.go delete mode 100644 service/updates/restart.go diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 630f5827..01d71402 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -55,7 +55,10 @@ func initialize() *service.Instance { // Create instance. var execCmdLine bool - instance, err := service.New(&service.ServiceConfig{}) + instance, err := service.New(&service.ServiceConfig{ + IsRunningAsService: isRunningAsService(), + DefaultRestartCommand: defaultRestartCommand, + }) switch { case err == nil: // Continue diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go index aa8d9a60..e8268dd7 100644 --- a/cmds/portmaster-core/main_linux.go +++ b/cmds/portmaster-core/main_linux.go @@ -4,14 +4,19 @@ import ( "fmt" "log/slog" "os" + "os/exec" "os/signal" "syscall" "time" + processInfo "github.com/shirou/gopsutil/process" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service" ) +var defaultRestartCommand = exec.Command("systemctl", "restart", "portmaster") + func run(instance *service.Instance) { // Set default log level. log.SetLogLevel(log.WarningLevel) @@ -98,3 +103,20 @@ func run(instance *service.Instance) { os.Exit(instance.ExitCode()) } + +func isRunningAsService() bool { + // Get the current process ID + pid := os.Getpid() + + currentProcess, err := processInfo.NewProcess(int32(pid)) + if err != nil { + return false + } + + ppid, err := currentProcess.Ppid() + if err != nil { + return false + } + // Check if the parent process ID is 1 == init system + return ppid == 1 +} diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go index 9b3798c3..cf47760e 100644 --- a/cmds/portmaster-core/main_windows.go +++ b/cmds/portmaster-core/main_windows.go @@ -9,6 +9,7 @@ import ( "fmt" "log/slog" "os" + "os/exec" "os/signal" "sync" "syscall" @@ -24,6 +25,8 @@ var ( // wait groups runWg sync.WaitGroup finishWg sync.WaitGroup + + defaultRestartCommand = exec.Command("sc.exe", "restart", "PortmasterCore") ) const serviceName = "PortmasterCore" @@ -168,3 +171,11 @@ func registerSignalHandler(instance *service.Instance) { } }() } + +func isRunningAsService() bool { + isService, err := svc.IsWindowsService() + if err != nil { + return false + } + return isService +} diff --git a/service/config.go b/service/config.go index 85a98603..8823ba5a 100644 --- a/service/config.go +++ b/service/config.go @@ -1,3 +1,8 @@ package service -type ServiceConfig struct{} +import "os/exec" + +type ServiceConfig struct { + IsRunningAsService bool + DefaultRestartCommand *exec.Cmd +} diff --git a/service/core/api.go b/service/core/api.go index 2c465ccc..bb63464b 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -19,7 +19,6 @@ import ( "github.com/safing/portmaster/service/process" "github.com/safing/portmaster/service/resolver" "github.com/safing/portmaster/service/status" - "github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/spn/captain" ) @@ -149,8 +148,8 @@ func shutdown(_ *api.Request) (msg string, err error) { func restart(_ *api.Request) (msg string, err error) { log.Info("core: user requested restart via action") - // Let the updates module handle restarting. - updates.RestartNow() + // Trigger restart + module.instance.Restart() return "restart initiated", nil } diff --git a/service/core/core.go b/service/core/core.go index 60ad5857..1f41285c 100644 --- a/service/core/core.go +++ b/service/core/core.go @@ -114,6 +114,7 @@ func New(instance instance) (*Core, error) { type instance interface { Shutdown() + Restart() AddWorkerInfoToDebugInfo(di *debug.Info) BinaryUpdates() *updates.Updates IntelUpdates() *updates.Updates diff --git a/service/instance.go b/service/instance.go index 4c522a34..7887b0ef 100644 --- a/service/instance.go +++ b/service/instance.go @@ -4,14 +4,17 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" go_runtime "runtime" + "strings" "sync/atomic" "time" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/database/dbmodule" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/rng" @@ -101,7 +104,9 @@ type Instance struct { sluice *sluice.SluiceModule terminal *terminal.TerminalModule - CommandLineOperation func() error + CommandLineOperation func() error + isRunningAsService bool + defaultRestartCommand *exec.Cmd } func getCurrentBinaryFolder() (string, error) { @@ -178,6 +183,8 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx // Create instance to pass it to modules. instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) + instance.isRunningAsService = svcCfg.IsRunningAsService + instance.defaultRestartCommand = svcCfg.DefaultRestartCommand var err error @@ -671,7 +678,7 @@ func (i *Instance) Restart() { i.core.EventRestart.Submit(struct{}{}) time.Sleep(10 * time.Millisecond) - i.shutdown(RestartExitCode) + i.serviceRestart() } // Shutdown asynchronously stops the instance. @@ -700,6 +707,25 @@ func (i *Instance) shutdown(exitCode int) { }) } +func (i *Instance) serviceRestart() { + if !i.isRunningAsService { + i.shutdown(RestartExitCode) + return + } + cmd := i.defaultRestartCommand + + // Check if user defined custom command for restarting the service. + restartCommand, exists := os.LookupEnv("PORTMASTER_RESTART_COMMAND") + + log.Debugf(`instance: running command "%s", %v`, restartCommand, exists) + if exists && restartCommand != "" { + commandSplit := strings.Split(restartCommand, " ") + cmd = exec.Command(commandSplit[0], commandSplit[1:]...) + } + log.Debugf("instance: running command %s", cmd.String()) + _ = cmd.Run() +} + // Stopping returns whether the instance is shutting down. func (i *Instance) Stopping() bool { return i.ctx.Err() != nil diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 3cc35624..e5d1d54d 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -16,6 +16,7 @@ import ( "strings" semver "github.com/hashicorp/go-version" + "github.com/safing/portmaster/base/log" ) diff --git a/service/updates/index.go b/service/updates/index.go deleted file mode 100644 index de15a98b..00000000 --- a/service/updates/index.go +++ /dev/null @@ -1 +0,0 @@ -package updates diff --git a/service/updates/main.go b/service/updates/main.go deleted file mode 100644 index de15a98b..00000000 --- a/service/updates/main.go +++ /dev/null @@ -1 +0,0 @@ -package updates diff --git a/service/updates/module.go b/service/updates/module.go index 625fe395..cd29c3f7 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -150,7 +150,7 @@ func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error { // Perform restart. u.instance.Restart() } else { - // Update completed and no restart was needed. Submit an event. + // Update completed and no restart is needed. Submit an event. u.EventResourcesUpdated.Submit(struct{}{}) } return nil diff --git a/service/updates/registry.go b/service/updates/registry.go index 4a797947..8ffc461f 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -8,6 +8,7 @@ import ( "path/filepath" semver "github.com/hashicorp/go-version" + "github.com/safing/portmaster/base/log" ) diff --git a/service/updates/restart.go b/service/updates/restart.go deleted file mode 100644 index 66ee82d8..00000000 --- a/service/updates/restart.go +++ /dev/null @@ -1,85 +0,0 @@ -package updates - -import ( - "sync" - "time" - - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/log" -) - -var ( - // RebootOnRestart defines whether the whole system, not just the service, - // should be restarted automatically when triggering a restart internally. - RebootOnRestart bool - - restartPending = abool.New() - restartTriggered = abool.New() - - restartTime time.Time - restartTimeLock sync.Mutex -) - -// IsRestarting returns whether a restart has been triggered. -func IsRestarting() bool { - return restartTriggered.IsSet() -} - -// RestartIsPending returns whether a restart is pending. -func RestartIsPending() (pending bool, restartAt time.Time) { - if restartPending.IsNotSet() { - return false, time.Time{} - } - - restartTimeLock.Lock() - defer restartTimeLock.Unlock() - - return true, restartTime -} - -// DelayedRestart triggers a restart of the application by shutting down the -// module system gracefully and returning with RestartExitCode. The restart -// may be further delayed by up to 10 minutes by the internal task scheduling -// system. This only works if the process is managed by portmaster-start. -func DelayedRestart(delay time.Duration) { - // Check if restart is already pending. - if !restartPending.SetToIf(false, true) { - return - } - - // Schedule the restart task. - log.Warningf("updates: restart triggered, will execute in %s", delay) - restartAt := time.Now().Add(delay) - // module.restartWorkerMgr.Delay(delay) - - // Set restartTime. - restartTimeLock.Lock() - defer restartTimeLock.Unlock() - restartTime = restartAt -} - -// AbortRestart aborts a (delayed) restart. -func AbortRestart() { - if restartPending.SetToIf(true, false) { - log.Warningf("updates: restart aborted") - - // Cancel schedule. - // module.restartWorkerMgr.Delay(0) - } -} - -// TriggerRestartIfPending triggers an automatic restart, if one is pending. -// This can be used to prepone a scheduled restart if the conditions are preferable. -func TriggerRestartIfPending() { - // if restartPending.IsSet() { - // module.restartWorkerMgr.Go() - // } -} - -// RestartNow immediately executes a restart. -// This only works if the process is managed by portmaster-start. -func RestartNow() { - restartPending.Set() - // module.restartWorkerMgr.Go() -} diff --git a/spn/captain/hooks.go b/spn/captain/hooks.go index d21bd67b..6e1b1010 100644 --- a/spn/captain/hooks.go +++ b/spn/captain/hooks.go @@ -1,7 +1,6 @@ package captain import ( - "github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/spn/conf" "github.com/safing/portmaster/spn/docks" ) @@ -41,5 +40,7 @@ func updateConnectionStatus() { return } } - updates.TriggerRestartIfPending() + + // TODO(vladimir): what was this needed for? + // updates.TriggerRestartIfPending() } diff --git a/spn/captain/intel.go b/spn/captain/intel.go index 68b7a74d..ac1145a6 100644 --- a/spn/captain/intel.go +++ b/spn/captain/intel.go @@ -44,7 +44,7 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { // Check if there is something to do. // TODO(vladimir): is update check needed - if intelResource != nil { //&& !intelResource.UpgradeAvailable() { + if intelResource != nil { // && !intelResource.UpgradeAvailable() { return nil } From f0272766c160575169a125c9509de920397f8e2f Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 27 Sep 2024 17:07:01 +0300 Subject: [PATCH 21/62] [WIP] Add restart service command for windows --- cmds/portmaster-core/main.go | 5 +- cmds/portmaster-core/main_linux.go | 28 +++++++++- cmds/portmaster-core/main_windows.go | 84 +++++++++++++--------------- service/config.go | 7 +-- service/instance.go | 34 ++--------- 5 files changed, 72 insertions(+), 86 deletions(-) diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 01d71402..630f5827 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -55,10 +55,7 @@ func initialize() *service.Instance { // Create instance. var execCmdLine bool - instance, err := service.New(&service.ServiceConfig{ - IsRunningAsService: isRunningAsService(), - DefaultRestartCommand: defaultRestartCommand, - }) + instance, err := service.New(&service.ServiceConfig{}) switch { case err == nil: // Continue diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go index e8268dd7..e0145822 100644 --- a/cmds/portmaster-core/main_linux.go +++ b/cmds/portmaster-core/main_linux.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "os/signal" + "strings" "syscall" "time" @@ -15,8 +16,6 @@ import ( "github.com/safing/portmaster/service" ) -var defaultRestartCommand = exec.Command("systemctl", "restart", "portmaster") - func run(instance *service.Instance) { // Set default log level. log.SetLogLevel(log.WarningLevel) @@ -101,9 +100,34 @@ func run(instance *service.Instance) { printStackTo(os.Stdout, "PRINTING STACK ON EXIT") } + // Check if restart was trigger and send start service command if true. + if isRunningAsService() && instance.ShouldRestart { + _ = runServiceRestart() + } + os.Exit(instance.ExitCode()) } +func runServiceRestart() error { + // Check if user defined custom command for restarting the service. + restartCommand, exists := os.LookupEnv("PORTMASTER_RESTART_COMMAND") + + // Run the service restart + if exists && restartCommand != "" { + log.Debugf(`instance: running custom restart command: "%s"`, restartCommand) + commandSplit := strings.Split(restartCommand, " ") + cmd := exec.Command(commandSplit[0], commandSplit[1:]...) + _ = cmd.Run() + } else { + cmd := exec.Command("systemctl", "restart", "portmaster") + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed run restart command: %w", err) + } + + } + return nil +} + func isRunningAsService() bool { // Get the current process ID pid := os.Getpid() diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go index cf47760e..7cd1c5bf 100644 --- a/cmds/portmaster-core/main_windows.go +++ b/cmds/portmaster-core/main_windows.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "os/signal" - "sync" "syscall" "time" @@ -21,14 +20,6 @@ import ( "golang.org/x/sys/windows/svc/debug" ) -var ( - // wait groups - runWg sync.WaitGroup - finishWg sync.WaitGroup - - defaultRestartCommand = exec.Command("sc.exe", "restart", "PortmasterCore") -) - const serviceName = "PortmasterCore" type windowsService struct { @@ -45,13 +36,15 @@ service: for { select { case <-ws.instance.Stopped(): - changes <- svc.Status{State: svc.StopPending} + log.Infof("instance stopped") break service case c := <-changeRequests: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: + log.Debugf("received shutdown command") + changes <- svc.Status{State: svc.StopPending} ws.instance.Shutdown() default: log.Errorf("unexpected control request: #%d", c) @@ -59,9 +52,6 @@ service: } } - // wait until everything else is finished - // finishWg.Wait() - log.Shutdown() // send stopped status @@ -75,11 +65,13 @@ service: func run(instance *service.Instance) error { log.SetLogLevel(log.WarningLevel) _ = log.Start() + // check if we are running interactively isService, err := svc.IsWindowsService() if err != nil { return fmt.Errorf("could not determine if running interactively: %s", err) } + // select service run type svcRun := svc.Run if !isService { @@ -88,41 +80,20 @@ func run(instance *service.Instance) error { go registerSignalHandler(instance) } - runWg.Add(1) - // run service client - go func() { - sErr := svcRun(serviceName, &windowsService{ - instance: instance, - }) - if sErr != nil { - log.Infof("shuting down service with error: %s", sErr) - } else { - log.Infof("shuting down service") - } - runWg.Done() - }() + sErr := svcRun(serviceName, &windowsService{ + instance: instance, + }) + if sErr != nil { + fmt.Printf("shuting down service with error: %s", sErr) + } else { + fmt.Printf("shuting down service") + } - // finishWg.Add(1) - // run service - // go func() { - // // run slightly delayed - // time.Sleep(250 * time.Millisecond) - - // if err != nil { - // fmt.Printf("instance start failed: %s\n", err) - - // // Print stack on start failure, if enabled. - // if printStackOnExit { - // printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - // } - - // } - // runWg.Done() - // finishWg.Done() - // }() - - runWg.Wait() + // Check if restart was trigger and send start service command if true. + if isRunningAsService() && instance.ShouldRestart { + _ = runServiceRestart() + } return err } @@ -179,3 +150,24 @@ func isRunningAsService() bool { } return isService } + +func runServiceRestart() error { + // Script that wait for portmaster service status to change to stop + // and then sends a start command for the same service. + command := ` +$serviceName = "PortmasterCore" +while ((Get-Service -Name $serviceName).Status -ne 'Stopped') { + Start-Sleep -Seconds 1 +} +sc.exe start $serviceName` + + // Create the command to execute the PowerShell script + cmd := exec.Command("powershell.exe", "-Command", command) + // Start the command. The script will continue even after the parent process exits. + err := cmd.Start() + if err != nil { + return err + } + + return nil +} diff --git a/service/config.go b/service/config.go index 8823ba5a..85a98603 100644 --- a/service/config.go +++ b/service/config.go @@ -1,8 +1,3 @@ package service -import "os/exec" - -type ServiceConfig struct { - IsRunningAsService bool - DefaultRestartCommand *exec.Cmd -} +type ServiceConfig struct{} diff --git a/service/instance.go b/service/instance.go index 7887b0ef..67682f1b 100644 --- a/service/instance.go +++ b/service/instance.go @@ -4,17 +4,14 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" go_runtime "runtime" - "strings" "sync/atomic" "time" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/database/dbmodule" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/rng" @@ -104,9 +101,8 @@ type Instance struct { sluice *sluice.SluiceModule terminal *terminal.TerminalModule - CommandLineOperation func() error - isRunningAsService bool - defaultRestartCommand *exec.Cmd + CommandLineOperation func() error + ShouldRestart bool } func getCurrentBinaryFolder() (string, error) { @@ -183,8 +179,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx // Create instance to pass it to modules. instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) - instance.isRunningAsService = svcCfg.IsRunningAsService - instance.defaultRestartCommand = svcCfg.DefaultRestartCommand var err error @@ -275,6 +269,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx if err != nil { return instance, fmt.Errorf("create customlist module: %w", err) } + instance.status, err = status.New(instance) if err != nil { return instance, fmt.Errorf("create status module: %w", err) @@ -678,7 +673,9 @@ func (i *Instance) Restart() { i.core.EventRestart.Submit(struct{}{}) time.Sleep(10 * time.Millisecond) - i.serviceRestart() + // Set the restart flag and shutdown. + i.ShouldRestart = true + i.shutdown(RestartExitCode) } // Shutdown asynchronously stops the instance. @@ -707,25 +704,6 @@ func (i *Instance) shutdown(exitCode int) { }) } -func (i *Instance) serviceRestart() { - if !i.isRunningAsService { - i.shutdown(RestartExitCode) - return - } - cmd := i.defaultRestartCommand - - // Check if user defined custom command for restarting the service. - restartCommand, exists := os.LookupEnv("PORTMASTER_RESTART_COMMAND") - - log.Debugf(`instance: running command "%s", %v`, restartCommand, exists) - if exists && restartCommand != "" { - commandSplit := strings.Split(restartCommand, " ") - cmd = exec.Command(commandSplit[0], commandSplit[1:]...) - } - log.Debugf("instance: running command %s", cmd.String()) - _ = cmd.Run() -} - // Stopping returns whether the instance is shutting down. func (i *Instance) Stopping() bool { return i.ctx.Err() != nil From 97ce3c087cb9699c7e3ee77d855d21e1b1832c62 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 30 Sep 2024 11:33:21 +0300 Subject: [PATCH 22/62] [WIP] Fix uninstaller deletion bug. --- service/updates/module.go | 15 ++++++++++----- service/updates/registry.go | 24 +++++++++--------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/service/updates/module.go b/service/updates/module.go index cd29c3f7..5df50c66 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -1,6 +1,7 @@ package updates import ( + "errors" "fmt" "runtime" "time" @@ -22,10 +23,14 @@ const ( ResourceUpdateEvent = "resource update" ) -// UserAgent is an HTTP User-Agent that is used to add -// more context to requests made by the registry when -// fetching resources from the update server. -var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) +var ( + // UserAgent is an HTTP User-Agent that is used to add + // more context to requests made by the registry when + // fetching resources from the update server. + UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) + + ErrNotFound error = errors.New("file not found") +) // UpdateIndex holds the configuration for the updates module. type UpdateIndex struct { @@ -193,7 +198,7 @@ func (u *Updates) GetRootPath() string { return u.registry.dir } -// GetFile returns the path of a file given the name. +// GetFile returns the path of a file given the name. Returns ErrNotFound if file is not found. func (u *Updates) GetFile(id string) (*File, error) { file, ok := u.registry.files[id] if ok { diff --git a/service/updates/registry.go b/service/updates/registry.go index 8ffc461f..dd57af1d 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -1,7 +1,6 @@ package updates import ( - "errors" "fmt" "io" "os" @@ -71,13 +70,7 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { // Make sure purge dir is empty. _ = os.RemoveAll(r.purgeDir) - // Read all files in the current version folder. - files, err := os.ReadDir(r.dir) - if err != nil { - return err - } - - // Create purge dir. Calling this after ReadDIr is important. + // Create purge dir. err = os.MkdirAll(r.purgeDir, defaultDirMode) if err != nil { return fmt.Errorf("failed to create directory: %w", err) @@ -85,12 +78,11 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { // Move current version files into purge folder. log.Debugf("updates: removing the old version") - for _, file := range files { - currentFilepath := filepath.Join(r.dir, file.Name()) - purgePath := filepath.Join(r.purgeDir, file.Name()) - err := moveFile(currentFilepath, purgePath) + for _, file := range r.files { + purgePath := filepath.Join(r.purgeDir, file.id) + err := moveFile(file.path, purgePath) if err != nil { - return fmt.Errorf("failed to move file %s: %w", currentFilepath, err) + return fmt.Errorf("failed to move file %s: %w", file.path, err) } } @@ -206,20 +198,22 @@ type File struct { sha256 string } +// Identifier return the id of the file witch is the same as the filename. func (f *File) Identifier() string { return f.id } +// Path returns the path + filename of the file. func (f *File) Path() string { return f.path } +// Version returns the version of the file. (currently not filled). func (f *File) Version() string { return f.version } +// Sha256 returns the sha356 sum of the file. func (f *File) Sha256() string { return f.sha256 } - -var ErrNotFound error = errors.New("file not found") From a452f0cbf629489545c2858cd983284a73fb080e Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 30 Sep 2024 14:33:05 +0300 Subject: [PATCH 23/62] [WIP] Add RecoverIPTables command on linux --- cmds/portmaster-core/main.go | 6 +- cmds/portmaster-core/main_linux.go | 14 +++++ cmds/portmaster-core/main_windows.go | 2 + cmds/portmaster-core/recover_linux.go | 83 +++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 cmds/portmaster-core/recover_linux.go diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 630f5827..7a5dc5a2 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -33,13 +33,15 @@ func init() { } func main() { + flag.Parse() + // Call platform specific checks, that will execute commands like "recover-iptables" + platformSpecificChecks() + instance := initialize() run(instance) } func initialize() *service.Instance { - flag.Parse() - // set information info.Set("Portmaster", "", "GPLv3") diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go index e0145822..8c7cc369 100644 --- a/cmds/portmaster-core/main_linux.go +++ b/cmds/portmaster-core/main_linux.go @@ -144,3 +144,17 @@ func isRunningAsService() bool { // Check if the parent process ID is 1 == init system return ppid == 1 } + +func platformSpecificChecks() { + // If flag is set. Run recover IP tables and exit. (Can be true only on linux) + if recoverIPTables { + exitCode := 0 + err := recoverIPTablesCmd() + if err != nil { + fmt.Printf("failed: %s", err) + exitCode = 1 + } + + os.Exit(exitCode) + } +} diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go index 7cd1c5bf..63cc22ff 100644 --- a/cmds/portmaster-core/main_windows.go +++ b/cmds/portmaster-core/main_windows.go @@ -171,3 +171,5 @@ sc.exe start $serviceName` return nil } + +func platformSpecificChecks() {} diff --git a/cmds/portmaster-core/recover_linux.go b/cmds/portmaster-core/recover_linux.go new file mode 100644 index 00000000..f13e5986 --- /dev/null +++ b/cmds/portmaster-core/recover_linux.go @@ -0,0 +1,83 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "strings" + + "github.com/hashicorp/go-multierror" + + "github.com/safing/portmaster/service/firewall/interception" +) + +var recoverIPTables bool + +func init() { + flag.BoolVar(&recoverIPTables, "recover-iptables", false, "recovers ip table rules") +} + +func recoverIPTablesCmd() error { + // interception.DeactiveNfqueueFirewall uses coreos/go-iptables + // which shells out to the /sbin/iptables binary. As a result, + // we don't get the errno of the actual error and need to parse the + // output instead. Make sure it's always english by setting LC_ALL=C + currentLocale := os.Getenv("LC_ALL") + _ = os.Setenv("LC_ALL", "C") + defer func() { + _ = os.Setenv("LC_ALL", currentLocale) + }() + + err := interception.DeactivateNfqueueFirewall() + if err == nil { + return nil + } + + // we don't want to show ErrNotExists to the user + // as that only means portmaster did the cleanup itself. + var mr *multierror.Error + if !errors.As(err, &mr) { + return err + } + + var filteredErrors *multierror.Error + for _, err := range mr.Errors { + // if we have a permission denied error, all errors will be the same + if strings.Contains(err.Error(), "Permission denied") { + return fmt.Errorf("failed to cleanup iptables: %w", os.ErrPermission) + } + + if !strings.Contains(err.Error(), "No such file or directory") { + filteredErrors = multierror.Append(filteredErrors, err) + } + } + + if filteredErrors != nil { + filteredErrors.ErrorFormat = formatNfqErrors + return filteredErrors.ErrorOrNil() + } + + return nil +} + +// func init() { +// rootCmd.AddCommand(recoverIPTablesCmd) +// } + +func formatNfqErrors(es []error) string { + if len(es) == 1 { + return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) + } + + points := make([]string, len(es)) + for i, err := range es { + // only display the very first line of each error + first := strings.Split(err.Error(), "\n")[0] + points[i] = fmt.Sprintf("* %s", first) + } + + return fmt.Sprintf( + "%d errors occurred:\n\t%s\n\n", + len(es), strings.Join(points, "\n\t")) +} From 3411e08500c03c8de4ed063696529214e29941ad Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 30 Sep 2024 18:48:38 +0300 Subject: [PATCH 24/62] [WIP] Fix cargo clippy lint build --- Earthfile | 51 +++---------------------- cmds/portmaster-core/recover_linux.go | 4 -- desktop/tauri/src-tauri/src/config.rs | 2 +- desktop/tauri/src-tauri/src/traymenu.rs | 7 ++-- desktop/tauri/src-tauri/src/window.rs | 2 +- packaging/linux/portmaster.service | 2 +- 6 files changed, 12 insertions(+), 56 deletions(-) diff --git a/Earthfile b/Earthfile index c2c43497..3468f6ac 100644 --- a/Earthfile +++ b/Earthfile @@ -459,20 +459,6 @@ tauri-build: DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" - # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling - # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. - # See documentation for externalBins for more information on how tauri searches for the binaries. - COPY (+go-build/output --CMDS="portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild - - # Place them in the correct folder with the rust target tripple attached. - FOR bin IN $(ls /tmp/gobuild) - # ${bin$.*} does not work in SET commands unfortunately so we use a shell - # snippet here: - RUN set -e ; \ - dest="./binaries/${bin}" ; \ - cp "/tmp/gobuild/${bin}" "${dest}" ; - END - # Just for debugging ... # RUN ls -R ./binaries @@ -634,39 +620,14 @@ tauri-release: END tauri-lint: - FROM +tauri-src - - # Clippy (rust linter) will try to build the project before it runs the linter. - # Make sure we have evrything needed to build the project. + FROM +rust-base ARG target="x86_64-unknown-linux-gnu" - # if we want tauri to create the installer bundles we also need to provide all external binaries - # we need to do some magic here because tauri expects the binaries to include the rust target tripple. - # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING - # function from below to parse the triple and guess wich GOOS and GOARCH we need. - RUN mkdir /tmp/gobuild - RUN mkdir ./binaries - - DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" - RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" - - # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling - # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. - # See documentation for externalBins for more information on how tauri searches for the binaries. - COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild - - # Place them in the correct folder with the rust target tripple attached. - FOR bin IN $(ls /tmp/gobuild) - # ${bin$.*} does not work in SET commands unfortunately so we use a shell - # snippet here: - RUN set -e ; \ - dest="./binaries/${bin}-${target}" ; \ - if [ -z "${bin##*.exe}" ]; then \ - dest="./binaries/${bin%.*}-${target}.exe" ; \ - fi ; \ - cp "/tmp/gobuild/${bin}" "${dest}" ; - END - DO rust+SET_CACHE_MOUNTS_ENV + WORKDIR /app + COPY --keep-ts ./assets ./assets + COPY --keep-ts ./desktop/tauri ./desktop/tauri + RUN mkdir -p ./desktop/angular/dist/tauri-builtin + WORKDIR /app/desktop/tauri/src-tauri RUN cargo clippy --all-targets --all-features -- -D warnings kext-build: diff --git a/cmds/portmaster-core/recover_linux.go b/cmds/portmaster-core/recover_linux.go index f13e5986..8ce989ba 100644 --- a/cmds/portmaster-core/recover_linux.go +++ b/cmds/portmaster-core/recover_linux.go @@ -61,10 +61,6 @@ func recoverIPTablesCmd() error { return nil } -// func init() { -// rootCmd.AddCommand(recoverIPTablesCmd) -// } - func formatNfqErrors(es []error) string { if len(es) == 1 { return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) diff --git a/desktop/tauri/src-tauri/src/config.rs b/desktop/tauri/src-tauri/src/config.rs index 60005295..6857ec99 100644 --- a/desktop/tauri/src-tauri/src/config.rs +++ b/desktop/tauri/src-tauri/src/config.rs @@ -16,7 +16,7 @@ pub struct Config { pub theme: Theme, } -const CONFIG_FILE_NAME: &'static str = "config.json"; +const CONFIG_FILE_NAME: &str = "config.json"; pub fn save(app: &AppHandle, config: Config) -> tauri::Result<()> { let config_dir = app.path().app_config_dir()?; diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 127c2194..634bca75 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -59,7 +59,7 @@ fn get_theme_mode() -> dark_light::Mode { if let Ok(value) = USER_THEME.read() { return *value.deref(); } - return dark_light::detect(); + dark_light::detect() } fn get_green_icon() -> &'static [u8] { @@ -88,8 +88,7 @@ fn get_blue_icon() -> &'static [u8] { fn get_red_icon() -> &'static [u8] { const LIGHT_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_red_64.png"); - const DARK_RED_ICON: &'static [u8] = - include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); + const DARK_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png"); match get_theme_mode() { dark_light::Mode::Light => DARK_RED_ICON, _ => LIGHT_RED_ICON, @@ -421,7 +420,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { match payload.parse::() { Ok(value) => { debug!("SPN status update: {}", value.status); - spn_status = value.status.clone(); + spn_status.clone_from(&value.status); update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); }, diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index 6cdcfe08..d2ddb1fd 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -98,7 +98,7 @@ pub fn hide_splash_window(app: &AppHandle) -> Result<()> { if let Some(window) = app.get_webview_window("splash") { return window.hide(); } - return Err(tauri::Error::WindowNotFound); + Err(tauri::Error::WindowNotFound) } pub fn set_window_icon(window: &WebviewWindow) { diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service index c16068d9..c48c8a24 100644 --- a/packaging/linux/portmaster.service +++ b/packaging/linux/portmaster.service @@ -36,7 +36,7 @@ StateDirectory=portmaster # TODO(ppacher): add --disable-software-updates once it's merged and the release process changed. WorkingDirectory=/var/lib/portmaster/data ExecStart=/usr/lib/portmaster/portmaster-core --data /var/lib/portmaster/data -devmode -- $PORTMASTER_ARGS -ExecStopPost=-/usr/bin/portmaster/portmaster-core recover-iptables +ExecStopPost=-/usr/lib/portmaster/portmaster-core -recover-iptables [Install] WantedBy=multi-user.target From 8e1f3c0ed95f231e15cf0363292b1a35bd9129a7 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 2 Oct 2024 15:37:39 +0300 Subject: [PATCH 25/62] [WIP] Add CI for building deb,rpm installers --- Earthfile | 233 +++++++---------- cmds/updatemgr/confirm.go | 20 -- cmds/updatemgr/main.go | 81 +++--- cmds/updatemgr/purge.go | 33 --- cmds/updatemgr/release.go | 195 --------------- cmds/updatemgr/scan.go | 49 ---- cmds/updatemgr/sign.go | 303 ----------------------- desktop/tauri/src-tauri/tauri.conf.json5 | 58 +++-- service/updates/bundle.go | 54 ++++ 9 files changed, 212 insertions(+), 814 deletions(-) delete mode 100644 cmds/updatemgr/confirm.go delete mode 100644 cmds/updatemgr/purge.go delete mode 100644 cmds/updatemgr/release.go delete mode 100644 cmds/updatemgr/scan.go delete mode 100644 cmds/updatemgr/sign.go diff --git a/Earthfile b/Earthfile index 3468f6ac..72504d8e 100644 --- a/Earthfile +++ b/Earthfile @@ -3,6 +3,7 @@ VERSION --arg-scope-and-set --global-cache 0.8 ARG --global go_version = 1.22 ARG --global node_version = 18 ARG --global rust_version = 1.79 +ARG --global tauri_version = "2.0.0-rc.8" ARG --global golangci_lint_version = 1.57.1 ARG --global go_builder_image = "golang:${go_version}-alpine" @@ -56,15 +57,14 @@ build: # Build Tauri app binaries: # ./dist/linux_amd64/portmaster-app + # ./dist/windows_amd64/portmaster-app + BUILD +tauri-build --target="x86_64-unknown-linux-gnu" + BUILD +tauri-build --target="x86_64-pc-windows-gnu" + + # TODO(vladimir): Build bundles # ./dist/linux_amd64/Portmaster-0.1.0-1.x86_64.rpm # ./dist/linux_amd64/Portmaster_0.1.0_amd64.deb - BUILD +tauri-build --target="x86_64-unknown-linux-gnu" - # TODO: - # BUILD +tauri-build --target="x86_64-pc-windows-gnu" - # Bild Tauri bundle for Windows: - # ./dist/windows_amd64/portmaster-app_vX-X-X.zip - BUILD +tauri-build-windows-bundle # Build UI assets: # ./dist/all/assets.zip @@ -82,7 +82,7 @@ angular-ci: tauri-ci: BUILD +tauri-build --target="x86_64-unknown-linux-gnu" - BUILD +tauri-build-windows-bundle + BUILD +tauri-build --target="x86_64-pc-windows-gnu" kext-ci: BUILD +kext-build @@ -349,6 +349,7 @@ angular-project: # Save portmaster UI as local artifact. IF [ "${project}" = "portmaster" ] SAVE ARTIFACT --keep-ts "./${project}.zip" AS LOCAL ${outputDir}/all/${project}-ui.zip + SAVE ARTIFACT --keep-ts "./${project}.zip" output/${project}.zip END # Build the angular projects (portmaster-UI and tauri-builtin) in dev mode @@ -420,7 +421,7 @@ rust-base: DO rust+INIT --keep_fingerprints=true # For now we need tauri-cli 2.0.0 for bulding - DO rust+CARGO --args="install tauri-cli --version ^2.0.0-beta" + DO rust+CARGO --args="install tauri-cli --version ${tauri_version} --locked" # Explicitly cache here. SAVE IMAGE --cache-hint @@ -434,7 +435,7 @@ tauri-src: # are preserved such that Rust's incremental compilation works correctly. COPY --keep-ts ./desktop/tauri/ . COPY assets/data ./../../assets/data - COPY packaging/linux ./../../packaging/linux + COPY packaging ./../../packaging COPY (+angular-project/output/tauri-builtin --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/") ./../angular/dist/tauri-builtin WORKDIR /app/tauri/src-tauri @@ -446,31 +447,14 @@ tauri-build: FROM +tauri-src ARG --required target - ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe)))|bundle/(deb|rpm)/.*\.(deb|rpm))" - ARG bundle="none" - - # if we want tauri to create the installer bundles we also need to provide all external binaries - # we need to do some magic here because tauri expects the binaries to include the rust target tripple. - # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING - # function from below to parse the triple and guess wich GOOS and GOARCH we need. - RUN mkdir /tmp/gobuild - RUN mkdir ./binaries + ARG output=".*/release/([^\./]+|([^\./]+\.(dll|exe)))" DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" - # Just for debugging ... - # RUN ls -R ./binaries - - # The following is exected to work but doesn't. for whatever reason cargo-sweep errors out on the windows-toolchain. - # - # DO rust+CARGO --args="tauri build --bundles none --ci --target=${target}" --output="release/[^/\.]+" - # - # For, now, we just directly mount the rust target cache and call cargo ourself. - DO rust+SET_CACHE_MOUNTS_ENV RUN rustup target add "${target}" - RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --ci --target="${target}" + RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --ci --target="${target}" --no-bundle DO rust+COPY_OUTPUT --output="${output}" # BUG(cross-compilation): @@ -489,127 +473,13 @@ tauri-build: RUN echo output: $(ls -R "target/${target}/release") # Binaries - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app" - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app.exe" - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/WebView2Loader.dll" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/WebView2Loader.dll" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster.exe" + # SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/WebView2Loader.dll" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/WebView2Loader.dll" - # Installers - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" - SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" ./output/portmaster + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" ./output/portmaster.exe -tauri-build-windows-bundle: - FROM +tauri-src - - ARG target="x86_64-pc-windows-gnu" - ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe))))" - ARG bundle="none" - - ARG GOOS=windows - ARG GOARCH=amd64 - ARG GOARM - - # The binaries will not be used but we still need to create them. Tauri will check for them. - RUN mkdir /tmp/gobuild - RUN mkdir ./binaries - - DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" - RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" - - # Our tauri app has externalBins configured so tauri will look for them when it finished compiling - # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. - # See documentation for externalBins for more information on how tauri searches for the binaries. - COPY (+go-build/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild - - # Place them in the correct folder with the rust target tripple attached. - FOR bin IN $(ls /tmp/gobuild) - # ${bin$.*} does not work in SET commands unfortunately so we use a shell - # snippet here: - RUN set -e ; \ - dest="./binaries/${bin}-${target}" ; \ - if [ -z "${bin##*.exe}" ]; then \ - dest="./binaries/${bin%.*}-${target}.exe" ; \ - fi ; \ - cp "/tmp/gobuild/${bin}" "${dest}" ; - END - - # Just for debugging ... - # RUN ls -R ./binaries - - DO rust+SET_CACHE_MOUNTS_ENV - RUN rustup target add "${target}" - RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --no-bundle --ci --target="${target}" - DO rust+COPY_OUTPUT --output="${output}" - - # Get version from git. - COPY .git . - LET version = "$(git tag --points-at || true)" - IF [ -z "${version}" ] - LET dev_version = "$(git describe --tags --first-parent --abbrev=0 || true)" - IF [ -n "${dev_version}" ] - SET version = "${dev_version}" - END - END - IF [ -z "${version}" ] - SET version = "v0.0.0" - END - ENV VERSION="${version}" - RUN echo "Version: $VERSION" - ENV VERSION_SUFFIX="$(echo $VERSION | tr '.' '-')" - RUN echo "Version Suffix: $VERSION_SUFFIX" - - RUN echo output: $(ls -R "target/${target}/release") - RUN mv "target/${target}/release/portmaster.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" - RUN zip "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" -j portmaster-app${VERSION_SUFFIX}.exe "target/${target}/release/WebView2Loader.dll" -j WebView2Loader.dll - SAVE ARTIFACT --if-exists "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" - -tauri-prep-windows: - FROM +angular-base --configuration=production - ARG target="x86_64-pc-windows-msvc" - - # if we want tauri to create the installer bundles we also need to provide all external binaries - # we need to do some magic here because tauri expects the binaries to include the rust target tripple. - # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING - # function from below to parse the triple and guess wich GOOS and GOARCH we need. - RUN mkdir /tmp/gobuild - RUN mkdir ./binaries - - DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" - RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" - - # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling - # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. - # See documentation for externalBins for more information on how tauri searches for the binaries. - - COPY (+go-build/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild - - # Place them in the correct folder with the rust target tripple attached. - FOR bin IN $(ls /tmp/gobuild) - # ${bin$.*} does not work in SET commands unfortunately so we use a shell - # snippet here: - RUN set -e ; \ - dest="./binaries/${bin}-${target}" ; \ - if [ -z "${bin##*.exe}" ]; then \ - dest="./binaries/${bin%.*}-${target}.exe" ; \ - fi ; \ - cp "/tmp/gobuild/${bin}" "${dest}" ; - END - - # Copy source - COPY --keep-ts ./desktop/tauri/src-tauri src-tauri - COPY --keep-ts ./assets assets - - # Build UI - ENV NODE_ENV="production" - RUN --no-cache ./node_modules/.bin/ng build --configuration production --base-href / "tauri-builtin" - - # Just for debugging ... - # RUN ls -R ./binaries - # RUN ls -R ./dist - - SAVE ARTIFACT "./dist/tauri-builtin" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/angular/dist/" - SAVE ARTIFACT "./src-tauri" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/tauri/src-tauri" - SAVE ARTIFACT "./binaries" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/tauri/src-tauri/" - SAVE ARTIFACT "./assets" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/assets" tauri-release: FROM ${work_image} @@ -624,11 +494,78 @@ tauri-lint: ARG target="x86_64-unknown-linux-gnu" WORKDIR /app + # Copy static files that are embedded inside the executable. COPY --keep-ts ./assets ./assets + + # Copy all the rust code COPY --keep-ts ./desktop/tauri ./desktop/tauri + + # Create a empty ui dir so it will satisfy the build. RUN mkdir -p ./desktop/angular/dist/tauri-builtin + + SAVE IMAGE --cache-hint + + # Run the linter. WORKDIR /app/desktop/tauri/src-tauri - RUN cargo clippy --all-targets --all-features -- -D warnings + RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo clippy --all-targets --all-features -- -D warnings + +tauri-bundle-linux: + FROM +rust-base + # ARG --required target + ARG target="x86_64-unknown-linux-gnu" + + WORKDIR /app/tauri + COPY --keep-ts ./desktop/tauri/ . + COPY assets/data ./../../assets/data + COPY packaging ./../../packaging + + WORKDIR /app/tauri/src-tauri + + SAVE IMAGE --cache-hint + + + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" + + # Build and copy the binaries + RUN mkdir -p target/${target}/release + COPY (+tauri-build/output/portmaster --target=x86_64-unknown-linux-gnu) ./target/${target}/release/portmaster + + + RUN mkdir -p binary + COPY (+go-build/output/portmaster-core --GOARCH=amd64 --GOOS=linux --CMDS=portmaster-core) ./binary/portmaster-core + + COPY (+assets/assets.zip) ./binary/assets.zip + COPY (+angular-project/output/portmaster.zip --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/) ./binary/portmaster.zip + + + # Download the intel data + RUN mkdir -p intel + + RUN wget -O ./intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240529-0-1.mmdb.gz" && \ + wget -O ./intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240529-0-1.mmdb.gz" && \ + gzip -d ./intel/geoipv4.mmdb.gz && \ + gzip -d ./intel/geoipv6.mmdb.gz + + RUN touch "./intel/index.dsd" + RUN touch "./intel/base.dsdl" + RUN touch "./intel/intermediate.dsdl" + RUN touch "./intel/urgent.dsdl" + + + # Generate index files + COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr + RUN ./updatemgr -dir "./binary" -name "Binary" > ./binary/bin-index.json + RUN ./updatemgr -dir "./intel" -name "Intel" > ./intel/intel-index.json + + RUN cat ./binary/bin-index.json + RUN cat ./intel/intel-index.json + + # build the installers + RUN cargo tauri bundle --ci --target="${target}" + + # Installers + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" + SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/" kext-build: FROM ${rust_builder_image} diff --git a/cmds/updatemgr/confirm.go b/cmds/updatemgr/confirm.go deleted file mode 100644 index 293faaf6..00000000 --- a/cmds/updatemgr/confirm.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "strings" -) - -func confirm(msg string) bool { - fmt.Printf("%s: [y|n] ", msg) - - scanner := bufio.NewScanner(os.Stdin) - ok := scanner.Scan() - if ok && strings.TrimSpace(scanner.Text()) == "y" { - return true - } - - return false -} diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go index acd9a0d4..519d94eb 100644 --- a/cmds/updatemgr/main.go +++ b/cmds/updatemgr/main.go @@ -1,58 +1,51 @@ package main import ( + "encoding/json" + "flag" "fmt" "os" - "path/filepath" - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils" + "github.com/safing/portmaster/service/updates" ) -var ( - registry *updater.ResourceRegistry - distDir string -) - -var rootCmd = &cobra.Command{ - Use: "updatemgr", - Short: "A simple tool to assist in the update and release process", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // Check if the distribution directory exists. - absDistPath, err := filepath.Abs(distDir) - if err != nil { - return fmt.Errorf("failed to get absolute path of distribution directory: %w", err) - } - _, err = os.Stat(absDistPath) - if err != nil { - return fmt.Errorf("failed to access distribution directory: %w", err) - } - - registry = &updater.ResourceRegistry{} - err = registry.Initialize(utils.NewDirStructure(absDistPath, 0o0755)) - if err != nil { - return err - } - - err = registry.ScanStorage("") - if err != nil { - return err - } - - return nil +var binaryMap = map[string]updates.Artifact{ + "portmaster-core": { + Platform: "linux_amd64", + }, + "portmaster-core.exe": { + Platform: "windows_amd64", + }, + "portmaster-kext.sys": { + Platform: "windows_amd64", }, - SilenceUsage: true, -} - -func init() { - flags := rootCmd.PersistentFlags() - flags.StringVar(&distDir, "dist-dir", "dist", "Set the distribution directory. Falls back to ./dist if available.") } func main() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) + dir := flag.String("dir", "", "path to the directory that contains the artifacts") + name := flag.String("name", "", "name of the bundle") + version := flag.String("version", "", "version of the bundle") + + flag.Parse() + if *dir == "" { + fmt.Fprintf(os.Stderr, "-dir parameter is required\n") + return } + if *name == "" { + fmt.Fprintf(os.Stderr, "-name parameter is required\n") + return + } + + bundle, err := updates.GenerateBundleFromDir(*name, *version, binaryMap, *dir) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to generate bundle: %s\n", err) + return + } + + bundleStr, err := json.MarshalIndent(&bundle, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal bundle: %s\n", err) + } + + fmt.Printf("%s", bundleStr) } diff --git a/cmds/updatemgr/purge.go b/cmds/updatemgr/purge.go deleted file mode 100644 index d5b456ee..00000000 --- a/cmds/updatemgr/purge.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/log" -) - -func init() { - rootCmd.AddCommand(purgeCmd) -} - -var purgeCmd = &cobra.Command{ - Use: "purge", - Short: "Remove old resource versions that are superseded by at least three versions", - RunE: purge, -} - -func purge(cmd *cobra.Command, args []string) error { - log.SetLogLevel(log.TraceLevel) - err := log.Start() - if err != nil { - fmt.Printf("failed to start logging: %s\n", err) - } - defer log.Shutdown() - - registry.SelectVersions() - registry.Purge(3) - - return nil -} diff --git a/cmds/updatemgr/release.go b/cmds/updatemgr/release.go deleted file mode 100644 index 0f5d596e..00000000 --- a/cmds/updatemgr/release.go +++ /dev/null @@ -1,195 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/updater" -) - -var ( - releaseCmd = &cobra.Command{ - Use: "release", - Short: "Release scans the distribution directory and creates registry indexes and the symlink structure", - Args: cobra.ExactArgs(1), - RunE: release, - } - preReleaseCmd = &cobra.Command{ - Use: "prerelease", - Short: "Stage scans the specified directory and loads the indexes - it then creates a staging index with all files newer than the stable and beta indexes", - Args: cobra.ExactArgs(1), - RunE: release, - } - preReleaseFrom string - resetPreReleases bool -) - -func init() { - rootCmd.AddCommand(releaseCmd) - rootCmd.AddCommand(preReleaseCmd) - - preReleaseCmd.Flags().StringVar(&preReleaseFrom, "from", "", "Make a pre-release based on the given channel") - _ = preReleaseCmd.MarkFlagRequired("from") - preReleaseCmd.Flags().BoolVar(&resetPreReleases, "reset", false, "Reset pre-release assets") -} - -func release(cmd *cobra.Command, args []string) error { - channel := args[0] - - // Check if we want to reset instead. - if resetPreReleases { - return removeFilesFromIndex(getChannelVersions(preReleaseFrom, true)) - } - - // Write new index. - err := writeIndex( - channel, - getChannelVersions(preReleaseFrom, false), - ) - if err != nil { - return err - } - - // Only when doing a release: - if preReleaseFrom == "" { - // Create symlinks to latest stable versions. - if !confirm("\nDo you want to write latest symlinks?") { - fmt.Println("aborted...") - return nil - } - symlinksDir := registry.StorageDir().ChildDir("latest", 0o755) - err = registry.CreateSymlinks(symlinksDir) - if err != nil { - return err - } - fmt.Println("written latest symlinks") - } - - return nil -} - -func writeIndex(channel string, versions map[string]string) error { - // Create new index file. - indexFile := &updater.IndexFile{ - Channel: channel, - Published: time.Now().UTC().Round(time.Second), - Releases: versions, - } - - // Export versions and format them. - confirmData, err := json.MarshalIndent(indexFile, "", " ") - if err != nil { - return err - } - - // Build index paths. - oldIndexPath := filepath.Join(registry.StorageDir().Path, channel+".json") - newIndexPath := filepath.Join(registry.StorageDir().Path, channel+".v2.json") - - // Print preview. - fmt.Printf("%s\n%s\n%s\n\n", channel, oldIndexPath, newIndexPath) - fmt.Println(string(confirmData)) - - // Ask for confirmation. - if !confirm("\nDo you want to write this index?") { - fmt.Println("aborted...") - return nil - } - - // Write indexes. - err = writeAsJSON(oldIndexPath, versions) - if err != nil { - return fmt.Errorf("failed to write %s: %w", oldIndexPath, err) - } - err = writeAsJSON(newIndexPath, indexFile) - if err != nil { - return fmt.Errorf("failed to write %s: %w", newIndexPath, err) - } - - return nil -} - -func writeAsJSON(path string, data any) error { - // Marshal to JSON. - jsonData, err := json.MarshalIndent(data, "", " ") - if err != nil { - return err - } - - // Write to disk. - err = os.WriteFile(path, jsonData, 0o0644) //nolint:gosec - if err != nil { - return err - } - - fmt.Printf("written %s\n", path) - return nil -} - -func removeFilesFromIndex(versions map[string]string) error { - // Print preview. - fmt.Println("To be deleted:") - for _, filePath := range versions { - fmt.Println(filePath) - } - - // Ask for confirmation. - if !confirm("\nDo you want to delete these files?") { - fmt.Println("aborted...") - return nil - } - - // Delete files. - for _, filePath := range versions { - err := os.Remove(filePath) - if err != nil { - return err - } - } - fmt.Println("deleted") - - return nil -} - -func getChannelVersions(prereleaseFrom string, storagePath bool) map[string]string { - if prereleaseFrom != "" { - registry.AddIndex(updater.Index{ - Path: prereleaseFrom + ".json", - PreRelease: false, - }) - err := registry.LoadIndexes(context.Background()) - if err != nil { - panic(err) - } - } - - // Sort all versions. - registry.SelectVersions() - export := registry.Export() - - // Go through all versions and save the highest version, if not stable or beta. - versions := make(map[string]string) - for _, rv := range export { - highestVersion := rv.Versions[0] - - // Ignore versions that are in the reference release channel. - if highestVersion.CurrentRelease { - continue - } - - // Add highest version of matching release channel. - if storagePath { - versions[rv.Identifier] = rv.GetFile().Path() - } else { - versions[rv.Identifier] = highestVersion.VersionNumber - } - } - - return versions -} diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go deleted file mode 100644 index 674a581b..00000000 --- a/cmds/updatemgr/scan.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(scanCmd) -} - -var scanCmd = &cobra.Command{ - Use: "scan", - Short: "Scan the specified directory and print the result", - RunE: scan, -} - -func scan(cmd *cobra.Command, args []string) error { - // Reset and rescan. - registry.ResetResources() - err := registry.ScanStorage("") - if err != nil { - return err - } - - // Export latest versions. - data, err := json.MarshalIndent(exportSelected(true), "", " ") - if err != nil { - return err - } - // Print them. - fmt.Println(string(data)) - - return nil -} - -func exportSelected(preReleases bool) map[string]string { - registry.SetUsePreReleases(preReleases) - registry.SelectVersions() - export := registry.Export() - - versions := make(map[string]string) - for _, rv := range export { - versions[rv.Identifier] = rv.SelectedVersion.VersionNumber - } - return versions -} diff --git a/cmds/updatemgr/sign.go b/cmds/updatemgr/sign.go deleted file mode 100644 index 02cfb15d..00000000 --- a/cmds/updatemgr/sign.go +++ /dev/null @@ -1,303 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - - "github.com/safing/jess" - "github.com/safing/jess/filesig" - "github.com/safing/jess/truststores" -) - -func init() { - rootCmd.AddCommand(signCmd) - - // Required argument: envelope - signCmd.PersistentFlags().StringVarP(&envelopeName, "envelope", "", "", - "specify envelope name used for signing", - ) - _ = signCmd.MarkFlagRequired("envelope") - - // Optional arguments: verbose, tsdir, tskeyring - signCmd.PersistentFlags().BoolVarP(&signVerbose, "verbose", "v", false, - "enable verbose output", - ) - signCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "", "", - "specify a truststore directory (default loaded from JESS_TS_DIR env variable)", - ) - signCmd.PersistentFlags().StringVarP(&trustStoreKeyring, "tskeyring", "", "", - "specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir", - ) - - // Subcommand for signing indexes. - signCmd.AddCommand(signIndexCmd) -} - -var ( - signCmd = &cobra.Command{ - Use: "sign", - Short: "Sign resources", - RunE: sign, - Args: cobra.NoArgs, - } - signIndexCmd = &cobra.Command{ - Use: "index", - Short: "Sign indexes", - RunE: signIndex, - Args: cobra.ExactArgs(1), - } - - envelopeName string - signVerbose bool -) - -func sign(cmd *cobra.Command, args []string) error { - // Setup trust store. - trustStore, err := setupTrustStore() - if err != nil { - return err - } - - // Get envelope. - signingEnvelope, err := trustStore.GetEnvelope(envelopeName) - if err != nil { - return err - } - - // Get all resources and iterate over all versions. - export := registry.Export() - var verified, signed, fails int - for _, rv := range export { - for _, version := range rv.Versions { - file := version.GetFile() - - // Check if there is an existing signature. - _, err := os.Stat(file.Path() + filesig.Extension) - switch { - case err == nil || errors.Is(err, fs.ErrExist): - // If the file exists, just verify. - fileData, err := filesig.VerifyFile( - file.Path(), - file.Path()+filesig.Extension, - file.SigningMetadata(), - trustStore, - ) - if err != nil { - fmt.Printf("[FAIL] signature error for %s: %s\n", file.Path(), err) - fails++ - } else { - if signVerbose { - fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file.Path(), getSignedByMany(fileData, trustStore)) - } - verified++ - } - - case errors.Is(err, fs.ErrNotExist): - // Attempt to sign file. - fileData, err := filesig.SignFile( - file.Path(), - file.Path()+filesig.Extension, - file.SigningMetadata(), - signingEnvelope, - trustStore, - ) - if err != nil { - fmt.Printf("[FAIL] failed to sign %s: %s\n", file.Path(), err) - fails++ - } else { - fmt.Printf("[SIGN] signed %s with %s\n", file.Path(), getSignedBySingle(fileData, trustStore)) - signed++ - } - - default: - // File access error. - fmt.Printf("[FAIL] failed to access %s: %s\n", file.Path(), err) - fails++ - } - } - } - - if verified > 0 { - fmt.Printf("[STAT] verified %d files\n", verified) - } - if signed > 0 { - fmt.Printf("[STAT] signed %d files\n", signed) - } - if fails > 0 { - return fmt.Errorf("signing or verification failed on %d files", fails) - } - return nil -} - -func signIndex(cmd *cobra.Command, args []string) error { - // Setup trust store. - trustStore, err := setupTrustStore() - if err != nil { - return err - } - - // Get envelope. - signingEnvelope, err := trustStore.GetEnvelope(envelopeName) - if err != nil { - return err - } - - // Resolve globs. - files := make([]string, 0, len(args)) - for _, arg := range args { - matches, err := filepath.Glob(arg) - if err != nil { - return err - } - files = append(files, matches...) - } - - // Go through all files. - var verified, signed, fails int - for _, file := range files { - sigFile := file + filesig.Extension - - // Ignore matches for the signatures. - if strings.HasSuffix(file, filesig.Extension) { - continue - } - - // Check if there is an existing signature. - _, err := os.Stat(sigFile) - switch { - case err == nil || errors.Is(err, fs.ErrExist): - // If the file exists, just verify. - fileData, err := filesig.VerifyFile( - file, - sigFile, - nil, - trustStore, - ) - if err == nil { - if signVerbose { - fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file, getSignedByMany(fileData, trustStore)) - } - verified++ - - // Indexes are expected to change, so just sign the index again if verification fails. - continue - } - - fallthrough - case errors.Is(err, fs.ErrNotExist): - // Attempt to sign file. - fileData, err := filesig.SignFile( - file, - sigFile, - nil, - signingEnvelope, - trustStore, - ) - if err != nil { - fmt.Printf("[FAIL] failed to sign %s: %s\n", file, err) - fails++ - } else { - fmt.Printf("[SIGN] signed %s with %s\n", file, getSignedBySingle(fileData, trustStore)) - signed++ - } - - default: - // File access error. - fmt.Printf("[FAIL] failed to access %s: %s\n", sigFile, err) - fails++ - } - } - - if verified > 0 { - fmt.Printf("[STAT] verified %d files", verified) - } - if signed > 0 { - fmt.Printf("[STAT] signed %d files", signed) - } - if fails > 0 { - return fmt.Errorf("signing failed on %d files", fails) - } - return nil -} - -var ( - trustStoreDir string - trustStoreKeyring string -) - -func setupTrustStore() (trustStore truststores.ExtendedTrustStore, err error) { - // Get trust store directory. - if trustStoreDir == "" { - trustStoreDir, _ = os.LookupEnv("JESS_TS_DIR") - if trustStoreDir == "" { - trustStoreDir, _ = os.LookupEnv("JESS_TSDIR") - } - } - if trustStoreDir != "" { - trustStore, err = truststores.NewDirTrustStore(trustStoreDir) - if err != nil { - return nil, err - } - } - - // Get trust store keyring. - if trustStore == nil { - if trustStoreKeyring == "" { - trustStoreKeyring, _ = os.LookupEnv("JESS_TS_KEYRING") - if trustStoreKeyring == "" { - trustStoreKeyring, _ = os.LookupEnv("JESS_TSKEYRING") - } - } - if trustStoreKeyring != "" { - trustStore, err = truststores.NewKeyringTrustStore(trustStoreKeyring) - if err != nil { - return nil, err - } - } - } - - // Truststore is mandatory. - if trustStore == nil { - return nil, errors.New("no truststore configured, please pass arguments or use env variables") - } - - return trustStore, nil -} - -func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string { - signedBy := make([]string, 0, len(fds)) - for _, fd := range fds { - if sig := fd.Signature(); sig != nil { - for _, seal := range sig.Signatures { - if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { - signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) - } else { - signedBy = append(signedBy, seal.ID) - } - } - } - } - return strings.Join(signedBy, " and ") -} - -func getSignedBySingle(fd *filesig.FileData, trustStore jess.TrustStore) string { - if sig := fd.Signature(); sig != nil { - signedBy := make([]string, 0, len(sig.Signatures)) - for _, seal := range sig.Signatures { - if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { - signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) - } else { - signedBy = append(signedBy, seal.ID) - } - } - return strings.Join(signedBy, " and ") - } - - return "" -} diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5 index 2d9de0d7..5bd85972 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json5 +++ b/desktop/tauri/src-tauri/tauri.conf.json5 @@ -59,18 +59,25 @@ ], "desktopTemplate": "../../../packaging/linux/portmaster.desktop", "files": { + // Service file "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", - "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", - "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", - "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", - "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", - "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", - "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", - "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", - "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", - "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", - "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", - "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", + + // Binary files + "/usr/lib/portmaster/bin-index.json": "binary/bin-index.json", + "/usr/lib/portmaster/portmaster-core": "binary/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binary/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binary/assets.zip", + + // Intel files + "/var/lib/portmaster/intel/intel-index.json": "intel/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "intel/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "intel/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "intel/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "intel/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "intel/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "intel/urgent.dsdl", + + // Shortcut "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", @@ -83,18 +90,25 @@ "desktopTemplate": "../../../packaging/linux/portmaster.desktop", "release": "1", "files": { + // Service file "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", - "/usr/lib/portmaster/bin-index.json": "binaries/bin-index.json", - "/usr/lib/portmaster/portmaster-core": "binaries/portmaster-core", - "/usr/lib/portmaster/portmaster.zip": "binaries/portmaster.zip", - "/usr/lib/portmaster/assets.zip": "binaries/assets.zip", - "/var/lib/portmaster/intel/intel-index.json": "binaries/intel-index.json", - "/var/lib/portmaster/intel/base.dsdl": "binaries/base.dsdl", - "/var/lib/portmaster/intel/geoipv4.mmdb": "binaries/geoipv4.mmdb", - "/var/lib/portmaster/intel/geoipv6.mmdb": "binaries/geoipv6.mmdb", - "/var/lib/portmaster/intel/index.dsd": "binaries/index.dsd", - "/var/lib/portmaster/intel/intermediate.dsdl": "binaries/intermediate.dsdl", - "/var/lib/portmaster/intel/urgent.dsdl": "binaries/urgent.dsdl", + + // Binary files + "/usr/lib/portmaster/bin-index.json": "binary/bin-index.json", + "/usr/lib/portmaster/portmaster-core": "binary/portmaster-core", + "/usr/lib/portmaster/portmaster.zip": "binary/portmaster.zip", + "/usr/lib/portmaster/assets.zip": "binary/assets.zip", + + // Intel files + "/var/lib/portmaster/intel/intel-index.json": "intel/intel-index.json", + "/var/lib/portmaster/intel/base.dsdl": "intel/base.dsdl", + "/var/lib/portmaster/intel/geoipv4.mmdb": "intel/geoipv4.mmdb", + "/var/lib/portmaster/intel/geoipv6.mmdb": "intel/geoipv6.mmdb", + "/var/lib/portmaster/intel/index.dsd": "intel/index.dsd", + "/var/lib/portmaster/intel/intermediate.dsdl": "intel/intermediate.dsdl", + "/var/lib/portmaster/intel/urgent.dsdl": "intel/urgent.dsdl", + + // Shortcut "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop" }, "postInstallScript": "../../../packaging/linux/postinst", diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 02b6089c..6cd2d430 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -120,3 +120,57 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } return true, nil } + +// GenerateBundleFromDir generates a bundle from a given folder. +func GenerateBundleFromDir(name string, version string, properties map[string]Artifact, dirPath string) (*Bundle, error) { + files, err := os.ReadDir(dirPath) + if err != nil { + return nil, err + } + artifacts := make([]Artifact, 0, len(files)) + for _, f := range files { + // Skip dirs + if f.IsDir() { + continue + } + + artifact := Artifact{ + Filename: f.Name(), + } + predefined, ok := properties[f.Name()] + // Check if caller supplied predefined settings for this artifact. + if ok { + // File that have compression may have different filename and artifact filename. (because of the extension) + // If caller did not specify the artifact filename set it as the same as the filename. + if predefined.Filename == "" { + predefined.Filename = f.Name() + } + artifact = predefined + } + content, err := os.ReadFile(filepath.Join(dirPath, f.Name())) + if err != nil { + return nil, err + } + + // Decompress if compression was applied to the file. + if artifact.Unpack != "" { + content, err = unpack(artifact.Unpack, content) + if err != nil { + return nil, err + } + } + + hash := sha256.Sum256(content) + hashStr := hex.EncodeToString(hash[:]) + artifact.SHA256 = hashStr + + artifacts = append(artifacts, artifact) + } + + return &Bundle{ + Name: name, + Version: version, + Artifacts: artifacts, + Published: time.Now(), + }, nil +} From 71f67a89362ec5bb4bc73290d4e67e78d53c1962 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 7 Oct 2024 11:26:27 +0300 Subject: [PATCH 26/62] [WIP] Improve CI build --- Earthfile | 95 ++++++--- cmds/updatemgr/main.go | 26 ++- desktop/tauri/src-tauri/Cargo.toml | 22 +-- desktop/tauri/src-tauri/templates/main.wxs | 2 +- .../src-tauri/templates/main_original.wxs | 2 +- .../windows/generate_windows_installers.ps1 | 46 +++++ service/updates/bundle.go | 54 ----- service/updates/bundlegeneration.go | 184 ++++++++++++++++++ 8 files changed, 327 insertions(+), 104 deletions(-) create mode 100644 packaging/windows/generate_windows_installers.ps1 create mode 100644 service/updates/bundlegeneration.go diff --git a/Earthfile b/Earthfile index 72504d8e..f5a24139 100644 --- a/Earthfile +++ b/Earthfile @@ -3,7 +3,7 @@ VERSION --arg-scope-and-set --global-cache 0.8 ARG --global go_version = 1.22 ARG --global node_version = 18 ARG --global rust_version = 1.79 -ARG --global tauri_version = "2.0.0-rc.8" +ARG --global tauri_version = "2.0.1" ARG --global golangci_lint_version = 1.57.1 ARG --global go_builder_image = "golang:${go_version}-alpine" @@ -509,7 +509,65 @@ tauri-lint: WORKDIR /app/desktop/tauri/src-tauri RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo clippy --all-targets --all-features -- -D warnings -tauri-bundle-linux: +release-prep: + FROM +rust-base + + # Linux specific + COPY (+tauri-build/output/portmaster --target="x86_64-unknown-linux-gnu") ./output/binary/linux_amd64/portmaster + COPY (+go-build/output/portmaster-core --GOARCH=amd64 --GOOS=linux --CMDS=portmaster-core) ./output/binary/linux_amd64/portmaster-core + + # Windows specific + COPY (+tauri-build/output/portmaster.exe --target="x86_64-pc-windows-gnu") ./output/binary/windows_amd64/portmaster.exe + COPY (+go-build/output/portmaster-core.exe --GOARCH=amd64 --GOOS=windows --CMDS=portmaster-core) ./output/binary/windows_amd64/portmaster-core.exe + # TODO(vladimir): figure out a way to get the lastest release of the kext. + RUN touch ./output/binary/windows_amd64/portmaster-kext.sys + + # All platforms + COPY (+assets/assets.zip) ./output/binary/all/assets.zip + COPY (+angular-project/output/portmaster.zip --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/) ./output/binary/all/portmaster.zip + + # Intel + # TODO(vladimir): figure out a way to download all latest intel data. + RUN mkdir -p ./output/intel + RUN wget -O ./output/intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240529-0-1.mmdb.gz" && \ + wget -O ./output/intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240529-0-1.mmdb.gz" + + RUN touch "./output/intel/index.dsd" + RUN touch "./output/intel/base.dsdl" + RUN touch "./output/intel/intermediate.dsdl" + RUN touch "./output/intel/urgent.dsdl" + + COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr + RUN ./updatemgr -dir "./output/binary" -name "Binary" > ./output/binary/bin-index.json + RUN ./updatemgr -dir "./output/intel" -name "Intel" > ./output/intel/intel-index.json + + # Intel Extracted (needed for the installers) + RUN mkdir -p ./output/intel_decompressed + RUN cp ./output/intel/intel-index.json ./output/intel_decompressed/intel-index.json + RUN gzip -dc ./output/intel/geoipv4.mmdb.gz > ./output/intel_decompressed/geoipv4.mmdb + RUN gzip -dc ./output/intel/geoipv6.mmdb.gz > ./output/intel_decompressed/geoipv6.mmdb + RUN touch "./output/intel_decompressed/index.dsd" + RUN touch "./output/intel_decompressed/base.dsdl" + RUN touch "./output/intel_decompressed/intermediate.dsdl" + RUN touch "./output/intel_decompressed/urgent.dsdl" + + # Save all artifacts to output folder + SAVE ARTIFACT --if-exists --keep-ts "output/binary/bin-index.json" AS LOCAL "${outputDir}/binary/bin-index.json" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" AS LOCAL "${outputDir}/binary/all/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" AS LOCAL "${outputDir}/binary/linux_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" AS LOCAL "${outputDir}/binary/windows_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel/*" AS LOCAL "${outputDir}/intel/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" AS LOCAL "${outputDir}/intel_decompressed/" + + # Save all artifacts to the container output folder so other containers can access it. + SAVE ARTIFACT --if-exists --keep-ts "output/binary/bin-index.json" "output/binary/bin-index.json" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" "output/binary/all/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" "output/binary/linux_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" "output/binary/windows_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel/*" "output/intel/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" "output/intel_decompressed/" + +installer-linux: FROM +rust-base # ARG --required target ARG target="x86_64-unknown-linux-gnu" @@ -523,42 +581,21 @@ tauri-bundle-linux: SAVE IMAGE --cache-hint - DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" # Build and copy the binaries RUN mkdir -p target/${target}/release - COPY (+tauri-build/output/portmaster --target=x86_64-unknown-linux-gnu) ./target/${target}/release/portmaster - + COPY (+release-prep/output/binary/linux_amd64/portmaster) ./target/${target}/release/portmaster RUN mkdir -p binary - COPY (+go-build/output/portmaster-core --GOARCH=amd64 --GOOS=linux --CMDS=portmaster-core) ./binary/portmaster-core - - COPY (+assets/assets.zip) ./binary/assets.zip - COPY (+angular-project/output/portmaster.zip --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/) ./binary/portmaster.zip - + COPY (+release-prep/output/binary/bin-index.json) ./binary/bin-index.json + COPY (+release-prep/output/binary/linux_amd64/portmaster-core) ./binary/portmaster-core + COPY (+release-prep/output/binary/all/portmaster.zip) ./binary/portmaster.zip + COPY (+release-prep/output/binary/all/assets.zip) ./binary/assets.zip # Download the intel data RUN mkdir -p intel - - RUN wget -O ./intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240529-0-1.mmdb.gz" && \ - wget -O ./intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240529-0-1.mmdb.gz" && \ - gzip -d ./intel/geoipv4.mmdb.gz && \ - gzip -d ./intel/geoipv6.mmdb.gz - - RUN touch "./intel/index.dsd" - RUN touch "./intel/base.dsdl" - RUN touch "./intel/intermediate.dsdl" - RUN touch "./intel/urgent.dsdl" - - - # Generate index files - COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr - RUN ./updatemgr -dir "./binary" -name "Binary" > ./binary/bin-index.json - RUN ./updatemgr -dir "./intel" -name "Intel" > ./intel/intel-index.json - - RUN cat ./binary/bin-index.json - RUN cat ./intel/intel-index.json + COPY (+release-prep/output/intel_decompressed/*) ./intel/ # build the installers RUN cargo tauri bundle --ci --target="${target}" diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go index 519d94eb..a157af07 100644 --- a/cmds/updatemgr/main.go +++ b/cmds/updatemgr/main.go @@ -10,17 +10,21 @@ import ( ) var binaryMap = map[string]updates.Artifact{ - "portmaster-core": { - Platform: "linux_amd64", + "geoipv4.mmdb.gz": { + Filename: "geoipv4.mmdb", + Unpack: "gz", }, - "portmaster-core.exe": { - Platform: "windows_amd64", - }, - "portmaster-kext.sys": { - Platform: "windows_amd64", + "geoipv6.mmdb.gz": { + Filename: "geoipv6.mmdb", + Unpack: "gz", }, } +var ignoreFiles = map[string]struct{}{ + "bin-index.json": {}, + "intel-index.json": {}, +} + func main() { dir := flag.String("dir", "", "path to the directory that contains the artifacts") name := flag.String("name", "", "name of the bundle") @@ -36,7 +40,13 @@ func main() { return } - bundle, err := updates.GenerateBundleFromDir(*name, *version, binaryMap, *dir) + settings := updates.BundleFileSettings{ + Name: *name, + Version: *version, + Properties: binaryMap, + IgnoreFiles: ignoreFiles, + } + bundle, err := updates.GenerateBundleFromDir(*dir, settings) if err != nil { fmt.Fprintf(os.Stderr, "failed to generate bundle: %s\n", err) return diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index f38c9679..78f87926 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -12,21 +12,21 @@ rust-version = "1.64" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "2.0.0-rc.7", features = [] } +tauri-build = { version = "2.0.1", features = [] } [dependencies] # Tauri -tauri = { version = "2.0.0-rc.8", features = ["tray-icon", "image-png", "config-json5", "devtools"] } -tauri-plugin-shell = "2.0.0-rc" -tauri-plugin-dialog = "2.0.0-rc" -tauri-plugin-clipboard-manager = "2.0.0-rc" -tauri-plugin-os = "2.0.0-rc" -tauri-plugin-single-instance = "2.0.0-rc" -tauri-plugin-notification = "2.0.0-rc" -tauri-plugin-log = "2.0.0-rc" -tauri-plugin-window-state = "2.0.0-rc" +tauri = { version = "2.0.1", features = ["tray-icon", "image-png", "config-json5", "devtools"] } +tauri-plugin-shell = "2.0.1" +tauri-plugin-dialog = "2.0.1" +tauri-plugin-clipboard-manager = "2.0.1" +tauri-plugin-os = "2.0.1" +tauri-plugin-single-instance = "2.0.1" +tauri-plugin-notification = "2.0.1" +tauri-plugin-log = "2.0.1" +tauri-plugin-window-state = "2.0.1" -tauri-cli = "2.0.0-rc.8" +tauri-cli = "2.0.1" clap_lex = "0.7.2" # General diff --git a/desktop/tauri/src-tauri/templates/main.wxs b/desktop/tauri/src-tauri/templates/main.wxs index f42ba777..f62f27a3 100644 --- a/desktop/tauri/src-tauri/templates/main.wxs +++ b/desktop/tauri/src-tauri/templates/main.wxs @@ -139,7 +139,7 @@ {{/each~}} - + {{#each file_associations as |association| ~}} {{#each association.ext as |ext| ~}} diff --git a/desktop/tauri/src-tauri/templates/main_original.wxs b/desktop/tauri/src-tauri/templates/main_original.wxs index 1b8116ed..b1d2672a 100644 --- a/desktop/tauri/src-tauri/templates/main_original.wxs +++ b/desktop/tauri/src-tauri/templates/main_original.wxs @@ -139,7 +139,7 @@ {{/each~}} - + {{#each file_associations as |association| ~}} {{#each association.ext as |ext| ~}} diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 new file mode 100644 index 00000000..ffa03cec --- /dev/null +++ b/packaging/windows/generate_windows_installers.ps1 @@ -0,0 +1,46 @@ +# Save the current directory +$originalDirectory = Get-Location + +$destinationDir = "desktop/tauri/src-tauri" +$binaryDir = "$destinationDir/binary" +$intelDir = "$destinationDir/intel" + +# Make sure distination folder exists. +if (-not (Test-Path -Path $binaryDir)) { + New-Item -ItemType Directory -Path $binaryDir > $null +} + +# Copy binary files +Copy-Item -Force -Path "dist/binary/bin-index.json" -Destination "$binaryDir/bin-index.json" +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-core.exe" -Destination "$binaryDir/portmaster-core.exe" +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-kext.sys" -Destination "$binaryDir/portmaster-kext.sys" +Copy-Item -Force -Path "dist/binary/all/portmaster.zip" -Destination "$binaryDir/portmaster.zip" +Copy-Item -Force -Path "dist/binary/all/assets.zip" -Destination "$binaryDir/assets.zip" +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster.exe" -Destination "$destinationDir/target/release/portmaster.exe" + +# Make sure distination folder exists. +if (-not (Test-Path -Path $intelDir)) { + New-Item -ItemType Directory -Path $intelDir > $null +} +# Copy intel data +Copy-Item -Force -Path "dist/intel_decompressed/*" -Destination "$intelDir/" + +Set-Location $destinationDir + +# Download tauri-cli +Invoke-WebRequest -Uri https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.0.1/cargo-tauri-x86_64-pc-windows-msvc.zip -OutFile tauri-cli.zip +Expand-Archive -Force tauri-cli.zip + +./tauri-cli/cargo-tauri.exe bundle + +$installerDist = "..\..\..\dist\windows_amd64\" +# Make sure distination folder exists. +if (-not (Test-Path -Path $installerDist)) { + New-Item -ItemType Directory -Path $installerDist > $null +} + +Copy-Item -Path ".\target\release\bundle\msi\*" -Destination $installerDist +Copy-Item -Path ".\target\release\bundle\nsis\*" -Destination $installerDist + +# Restore the original directory +Set-Location $originalDirectory \ No newline at end of file diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 6cd2d430..02b6089c 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -120,57 +120,3 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } return true, nil } - -// GenerateBundleFromDir generates a bundle from a given folder. -func GenerateBundleFromDir(name string, version string, properties map[string]Artifact, dirPath string) (*Bundle, error) { - files, err := os.ReadDir(dirPath) - if err != nil { - return nil, err - } - artifacts := make([]Artifact, 0, len(files)) - for _, f := range files { - // Skip dirs - if f.IsDir() { - continue - } - - artifact := Artifact{ - Filename: f.Name(), - } - predefined, ok := properties[f.Name()] - // Check if caller supplied predefined settings for this artifact. - if ok { - // File that have compression may have different filename and artifact filename. (because of the extension) - // If caller did not specify the artifact filename set it as the same as the filename. - if predefined.Filename == "" { - predefined.Filename = f.Name() - } - artifact = predefined - } - content, err := os.ReadFile(filepath.Join(dirPath, f.Name())) - if err != nil { - return nil, err - } - - // Decompress if compression was applied to the file. - if artifact.Unpack != "" { - content, err = unpack(artifact.Unpack, content) - if err != nil { - return nil, err - } - } - - hash := sha256.Sum256(content) - hashStr := hex.EncodeToString(hash[:]) - artifact.SHA256 = hashStr - - artifacts = append(artifacts, artifact) - } - - return &Bundle{ - Name: name, - Version: version, - Artifacts: artifacts, - Published: time.Now(), - }, nil -} diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go new file mode 100644 index 00000000..12ac8b20 --- /dev/null +++ b/service/updates/bundlegeneration.go @@ -0,0 +1,184 @@ +package updates + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "time" + + semver "github.com/hashicorp/go-version" +) + +type BundleFileSettings struct { + Name string + Version string + Properties map[string]Artifact + IgnoreFiles map[string]struct{} +} + +// GenerateBundleFromDir generates a bundle from a given folder. +func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bundle, error) { + bundleDirName := filepath.Base(bundleDir) + + artifacts := make([]Artifact, 0, 5) + err := filepath.Walk(bundleDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Skip folders + if info.IsDir() { + return nil + } + + identifier, version, ok := getIdentifierAndVersion(info.Name()) + if !ok { + identifier = info.Name() + } + + // Check if file is in the ignore list. + if _, ok := settings.IgnoreFiles[identifier]; ok { + return nil + } + + artifact := Artifact{} + + // Check if the caller provided properties for the artifact. + if p, ok := settings.Properties[identifier]; ok { + artifact = p + } + + // Set filename of artifact if not set by the caller. + if artifact.Filename == "" { + artifact.Filename = identifier + } + + artifact.Version = version + + // Fill the platform of the artifact + parentDir := filepath.Base(filepath.Dir(path)) + if parentDir != "all" && parentDir != bundleDirName { + artifact.Platform = parentDir + } + + // Fill the hash + hash, err := getSHA256(path, artifact.Unpack) + if err != nil { + return fmt.Errorf("failed to calculate hash of file: %s %w", path, err) + } + artifact.SHA256 = hash + + artifacts = append(artifacts, artifact) + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to walk the dir: %w", err) + } + + // Filter artifact so we have single version for each file + artifacts, err = selectLatestArtifacts(artifacts) + if err != nil { + return nil, fmt.Errorf("failed to select artifact version: %w", err) + } + + return &Bundle{ + Name: settings.Name, + Version: settings.Version, + Artifacts: artifacts, + Published: time.Now(), + }, nil +} + +func selectLatestArtifacts(artifacts []Artifact) ([]Artifact, error) { + artifactsMap := make(map[string]Artifact) + + for _, a := range artifacts { + // Make the key platform specific since there can be same filename for multiple platforms. + key := a.Filename + a.Platform + aMap, ok := artifactsMap[key] + if !ok { + artifactsMap[key] = a + continue + } + + if aMap.Version == "" || a.Version == "" { + return nil, fmt.Errorf("invalid mix version and non versioned files for: %s", a.Filename) + } + + mapVersion, err := semver.NewVersion(aMap.Version) + if err != nil { + return nil, fmt.Errorf("invalid version for artifact: %s", aMap.Filename) + } + + artifactVersion, err := semver.NewVersion(a.Version) + if err != nil { + return nil, fmt.Errorf("invalid version for artifact: %s", a.Filename) + } + + if mapVersion.LessThan(artifactVersion) { + artifactsMap[key] = a + } + } + + artifactsFiltered := make([]Artifact, 0, len(artifactsMap)) + for _, a := range artifactsMap { + artifactsFiltered = append(artifactsFiltered, a) + } + + return artifactsFiltered, nil +} + +func getSHA256(path string, unpackType string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", err + } + + // Decompress if compression was applied to the file. + if unpackType != "" { + content, err = unpack(unpackType, content) + if err != nil { + return "", err + } + } + + // Calculate hash + hash := sha256.Sum256(content) + return hex.EncodeToString(hash[:]), nil +} + +var ( + fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`) + rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?$`) +) + +func getIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) { + dirPath, filename := path.Split(versionedPath) + + // Extract version from filename. + rawVersion := fileVersionRegex.FindString(filename) + if rawVersion == "" { + // No version present in file, making it invalid. + return "", "", false + } + + // Trim the `_v` that gets caught by the regex and + // replace `-` with `.` to get the version string. + version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", 2) + + // Put the filename back together without version. + i := strings.Index(filename, rawVersion) + if i < 0 { + // extracted version not in string (impossible) + return "", "", false + } + filename = filename[:i] + filename[i+len(rawVersion):] + + // Put the full path back together and return it. + // `dirPath + filename` is guaranteed by path.Split() + return dirPath + filename, version, true +} From 19422726feb2c194b3ef504d7c17ac62292a3248 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 7 Oct 2024 15:01:34 +0300 Subject: [PATCH 27/62] [WIP] Improve windows installer build script --- .../windows/generate_windows_installers.ps1 | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 index ffa03cec..f55f0d89 100644 --- a/packaging/windows/generate_windows_installers.ps1 +++ b/packaging/windows/generate_windows_installers.ps1 @@ -1,3 +1,11 @@ +# Tested with docker image 'abrarov/msvc-2022:latest' +# sha256:f49435d194108cd56f173ad5bc6a27c70eed98b7e8cd54488f5acd85efbd51c9 + +# Run: +# Start powershell and cd to the root of the project. Then run: +# $path = Convert-Path . # Get the absolute path of the current directory +# docker run -it --rm -v "${path}:C:/app" -w "C:/app" abrarov/msvc-2022 powershell -NoProfile -File C:/app/packaging/windows/generate_windows_installer.ps1 + # Save the current directory $originalDirectory = Get-Location @@ -10,7 +18,7 @@ if (-not (Test-Path -Path $binaryDir)) { New-Item -ItemType Directory -Path $binaryDir > $null } -# Copy binary files +Write-Output "Copying binary files" Copy-Item -Force -Path "dist/binary/bin-index.json" -Destination "$binaryDir/bin-index.json" Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-core.exe" -Destination "$binaryDir/portmaster-core.exe" Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-kext.sys" -Destination "$binaryDir/portmaster-kext.sys" @@ -22,17 +30,28 @@ Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster.exe" -Destination " if (-not (Test-Path -Path $intelDir)) { New-Item -ItemType Directory -Path $intelDir > $null } -# Copy intel data + +Write-Output "Copying intel files" Copy-Item -Force -Path "dist/intel_decompressed/*" -Destination "$intelDir/" Set-Location $destinationDir -# Download tauri-cli +if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { + Write-Output "Installing rust toolchain..." + Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup.exe + ./rustup.exe install stable + $env:PATH += ";C:\Users\ContainerAdministrator\.rustup\toolchains\stable-x86_64-pc-windows-msvc\bin\" +} else { + Write-Output "'cargo' command is already available" +} + +Write-Output "Downloading tauri-cli" Invoke-WebRequest -Uri https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.0.1/cargo-tauri-x86_64-pc-windows-msvc.zip -OutFile tauri-cli.zip Expand-Archive -Force tauri-cli.zip - ./tauri-cli/cargo-tauri.exe bundle + +Write-Output "Copying generated bundles" $installerDist = "..\..\..\dist\windows_amd64\" # Make sure distination folder exists. if (-not (Test-Path -Path $installerDist)) { From a79be8b6a9fd6437b2a1be8b3066cc3beec0a3d9 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 7 Oct 2024 16:00:30 +0300 Subject: [PATCH 28/62] [WIP] Improve downloader resilience --- service/updates/bundle.go | 39 +++++-- service/updates/bundlegeneration.go | 7 +- service/updates/downloader.go | 163 +++++++++++++++++----------- service/updates/registry.go | 4 +- 4 files changed, 130 insertions(+), 83 deletions(-) diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 02b6089c..1ffe2abc 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -46,25 +46,40 @@ type Bundle struct { Artifacts []Artifact `json:"Artifacts"` } -func ParseBundle(indexFile string) (*Bundle, error) { - // Check if the file exists. - file, err := os.Open(indexFile) - if err != nil { - return nil, fmt.Errorf("failed to open index file: %w", err) - } - defer func() { _ = file.Close() }() - +// LoadBundle loads and parses a bundle from filepath. +func LoadBundle(indexFilepath string) (*Bundle, error) { // Read - content, err := io.ReadAll(file) + content, err := os.ReadFile(indexFilepath) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read index file: %w", err) } // Parse var bundle Bundle err = json.Unmarshal(content, &bundle) if err != nil { - return nil, fmt.Errorf("%s %w", indexFile, err) + return nil, fmt.Errorf("%s %w", indexFilepath, err) + } + + // Filter artifacts + filtered := make([]Artifact, 0) + for _, a := range bundle.Artifacts { + if a.Platform == "" || a.Platform == currentPlatform { + filtered = append(filtered, a) + } + } + bundle.Artifacts = filtered + + return &bundle, nil +} + +// ParseBundle parses a bundle from json string. +func ParseBundle(jsonContent string) (*Bundle, error) { + // Parse + var bundle Bundle + err := json.Unmarshal([]byte(jsonContent), &bundle) + if err != nil { + return nil, fmt.Errorf("failed to parse bundle: %w", err) } // Filter artifacts @@ -80,7 +95,7 @@ func ParseBundle(indexFile string) (*Bundle, error) { } // Verify checks if the files are present int the dataDir and have the correct hash. -func (bundle Bundle) Verify(dir string) error { +func (bundle *Bundle) Verify(dir string) error { for _, artifact := range bundle.Artifacts { artifactPath := filepath.Join(dir, artifact.Filename) isValid, err := checkIfFileIsValid(artifactPath, artifact) diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go index 12ac8b20..9b796880 100644 --- a/service/updates/bundlegeneration.go +++ b/service/updates/bundlegeneration.go @@ -140,7 +140,7 @@ func getSHA256(path string, unpackType string) (string, error) { // Decompress if compression was applied to the file. if unpackType != "" { - content, err = unpack(unpackType, content) + content, err = decompress(unpackType, content) if err != nil { return "", err } @@ -151,10 +151,7 @@ func getSHA256(path string, unpackType string) (string, error) { return hex.EncodeToString(hash[:]), nil } -var ( - fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`) - rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?$`) -) +var fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`) func getIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) { dirPath, filename := path.Split(versionedPath) diff --git a/service/updates/downloader.go b/service/updates/downloader.go index e5d1d54d..53ad2d8d 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -38,25 +38,51 @@ func CreateDownloader(index UpdateIndex) Downloader { } } -func (d *Downloader) downloadIndexFile(ctx context.Context) (err error) { +func (d *Downloader) downloadIndexFile(ctx context.Context) error { // Make sure dir exists _ = os.MkdirAll(d.dir, defaultDirMode) + var err error + var content string for _, url := range d.indexURLs { - err = d.downloadIndexFileFromURL(ctx, url) + content, err = d.downloadIndexFileFromURL(ctx, url) if err != nil { log.Warningf("updates: failed while downloading index file %s", err) continue } // Downloading was successful. + + bundle, err := ParseBundle(content) + if err != nil { + log.Warningf("updates: %s", err) + continue + } + // Parsing was successful + + version, err := semver.NewVersion(d.bundle.Version) + if err != nil { + log.Warningf("updates: failed to parse bundle version: %s", err) + continue + } + + // All checks passed. Set and exit the loop. + d.bundle = bundle + d.version = version err = nil break } - if err == nil { - err = d.parseBundle() + if err != nil { + return err } - return + // Write the content into a file. + indexFilepath := filepath.Join(d.dir, d.indexFile) + err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode) + if err != nil { + return fmt.Errorf("failed to write index file: %s", err) + } + + return nil } // Verify verifies if the downloaded files match the corresponding hash. @@ -72,7 +98,7 @@ func (d *Downloader) Verify() error { func (d *Downloader) parseBundle() error { indexFilepath := filepath.Join(d.dir, d.indexFile) var err error - d.bundle, err = ParseBundle(indexFilepath) + d.bundle, err = LoadBundle(indexFilepath) if err != nil { return err } @@ -84,11 +110,11 @@ func (d *Downloader) parseBundle() error { return nil } -func (d *Downloader) downloadIndexFileFromURL(ctx context.Context, url string) error { +func (d *Downloader) downloadIndexFileFromURL(ctx context.Context, url string) (string, error) { // Request the index file req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { - return fmt.Errorf("failed to create GET request to %s: %w", url, err) + return "", fmt.Errorf("failed to create GET request to %s: %w", url, err) } if UserAgent != "" { req.Header.Set("User-Agent", UserAgent) @@ -97,29 +123,22 @@ func (d *Downloader) downloadIndexFileFromURL(ctx context.Context, url string) e // Perform request resp, err := d.httpClient.Do(req) if err != nil { - return fmt.Errorf("failed GET request to %s: %w", url, err) + return "", fmt.Errorf("failed GET request to %s: %w", url, err) } defer func() { _ = resp.Body.Close() }() // Check the status code if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("received error from the server status code: %s", resp.Status) - } - // Create file - indexFilepath := filepath.Join(d.dir, d.indexFile) - file, err := os.Create(indexFilepath) - if err != nil { - return err - } - defer func() { _ = file.Close() }() - - // Write body of the response - _, err = io.Copy(file, resp.Body) - if err != nil { - return err + return "", fmt.Errorf("received error from the server status code: %s", resp.Status) } - return nil + // Read the content. + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(content), nil } // CopyMatchingFilesFromCurrent check if there the current bundle files has matching files with the new bundle and copies them if they match. @@ -193,27 +212,13 @@ func (d *Downloader) processArtifact(ctx context.Context, artifact Artifact, fil return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) } - // Download + // Download and verify log.Debugf("updates: downloading file: %s", artifact.Filename) - content, err := d.downloadFile(ctx, artifact.URLs) + content, err := d.downloadAndVerifyArtifact(ctx, artifact.URLs, artifact.Unpack, providedHash) if err != nil { return fmt.Errorf("failed to download artifact: %w", err) } - // Decompress - if artifact.Unpack != "" { - content, err = unpack(artifact.Unpack, content) - if err != nil { - return fmt.Errorf("failed to decompress artifact: %w", err) - } - } - - // Verify - hash := sha256.Sum256(content) - if !bytes.Equal(providedHash, hash[:]) { - return fmt.Errorf("failed to verify artifact: %s", artifact.Filename) - } - // Save tmpFilename := fmt.Sprintf("%s.download", filePath) err = os.WriteFile(tmpFilename, content, artifact.GetFileMode()) @@ -232,39 +237,69 @@ func (d *Downloader) processArtifact(ctx context.Context, artifact Artifact, fil return nil } -func (d *Downloader) downloadFile(ctx context.Context, urls []string) ([]byte, error) { +func (d *Downloader) downloadAndVerifyArtifact(ctx context.Context, urls []string, unpack string, expectedHash []byte) ([]byte, error) { + var err error + var content []byte + for _, url := range urls { - // Try to make the request - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + // Download + content, err = d.downloadFile(ctx, url) if err != nil { - log.Warningf("failed to create GET request to %s: %s", url, err) - continue - } - if UserAgent != "" { - req.Header.Set("User-Agent", UserAgent) - } - resp, err := d.httpClient.Do(req) - if err != nil { - log.Warningf("failed a get file request to: %s", err) - continue - } - defer func() { _ = resp.Body.Close() }() - - // Check if the server returned an error - if resp.StatusCode != http.StatusOK { - log.Warningf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) + err := fmt.Errorf("failed to download artifact from url: %s, %w", url, err) + log.Warningf("%s", err) continue } - content, err := io.ReadAll(resp.Body) - if err != nil { - log.Warningf("failed to read body of response: %s", err) + // Decompress + if unpack != "" { + content, err = decompress(unpack, content) + if err != nil { + err = fmt.Errorf("failed to decompress artifact: %w", err) + log.Warningf("%s", err) + continue + } + } + + // Calculate and verify hash + hash := sha256.Sum256(content) + if !bytes.Equal(expectedHash, hash[:]) { + err := fmt.Errorf("artifact hash does not match") + log.Warningf("%s", err) continue } + + // All file downloaded and verified. return content, nil } - return nil, fmt.Errorf("failed to download file from the provided urls") + return nil, err +} + +func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, error) { + // Try to make the request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create GET request to %s: %s", url, err) + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + resp, err := d.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed a get file request to: %s", err) + } + defer func() { _ = resp.Body.Close() }() + + // Check if the server returned an error + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read body of response: %s", err) + } + return content, nil } func (d *Downloader) deleteUnfinishedDownloads() error { @@ -286,7 +321,7 @@ func (d *Downloader) deleteUnfinishedDownloads() error { return nil } -func unpack(cType string, fileBytes []byte) ([]byte, error) { +func decompress(cType string, fileBytes []byte) ([]byte, error) { switch cType { case "zip": return decompressZip(fileBytes) diff --git a/service/updates/registry.go b/service/updates/registry.go index dd57af1d..9b07a742 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -35,7 +35,7 @@ func CreateRegistry(index UpdateIndex) (Registry, error) { } // Parse bundle var err error - registry.bundle, err = ParseBundle(filepath.Join(index.Directory, index.IndexFile)) + registry.bundle, err = LoadBundle(filepath.Join(index.Directory, index.IndexFile)) if err != nil { return Registry{}, err } @@ -57,7 +57,7 @@ func CreateRegistry(index UpdateIndex) (Registry, error) { func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { // Make sure provided update is valid indexFilepath := filepath.Join(downloadDir, indexFile) - bundle, err := ParseBundle(indexFilepath) + bundle, err := LoadBundle(indexFilepath) if err != nil { return fmt.Errorf("invalid update: %w", err) } From a8517cd65f3434b4cb08f4445de48b7edcfa192e Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 8 Oct 2024 09:47:29 +0300 Subject: [PATCH 29/62] [WIP] Fix minor bugs --- cmds/portmaster-core/main_linux.go | 15 ++++++--------- cmds/portmaster-core/main_windows.go | 2 +- .../src-tauri/templates/nsis_install_hooks.nsh | 10 +++++++--- service/updates/bundle.go | 5 ++++- service/updates/downloader.go | 2 +- spn/navigator/module_test.go | 4 ++-- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/main_linux.go index 8c7cc369..026cd249 100644 --- a/cmds/portmaster-core/main_linux.go +++ b/cmds/portmaster-core/main_linux.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "os/signal" - "strings" "syscall" "time" @@ -113,17 +112,15 @@ func runServiceRestart() error { restartCommand, exists := os.LookupEnv("PORTMASTER_RESTART_COMMAND") // Run the service restart + var cmd *exec.Cmd if exists && restartCommand != "" { log.Debugf(`instance: running custom restart command: "%s"`, restartCommand) - commandSplit := strings.Split(restartCommand, " ") - cmd := exec.Command(commandSplit[0], commandSplit[1:]...) - _ = cmd.Run() + cmd = exec.Command("sh", "-c", restartCommand) } else { - cmd := exec.Command("systemctl", "restart", "portmaster") - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed run restart command: %w", err) - } - + cmd = exec.Command("systemctl", "restart", "portmaster") + } + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed run restart command: %w", err) } return nil } diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/main_windows.go index 63cc22ff..8e0d9a6d 100644 --- a/cmds/portmaster-core/main_windows.go +++ b/cmds/portmaster-core/main_windows.go @@ -47,7 +47,7 @@ service: changes <- svc.Status{State: svc.StopPending} ws.instance.Shutdown() default: - log.Errorf("unexpected control request: #%d", c) + log.Errorf("unexpected control request: %+v", c) } } } diff --git a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh index a162a45d..b2ddd628 100644 --- a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh +++ b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh @@ -25,11 +25,15 @@ !macroend !macro NSIS_HOOK_POSTINSTALL - ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"' + ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"' $0 + IntCmp $0 0 +2 + MessageBox MB_OK "Failed to create PortmasterCore service." !macroend !macro NSIS_HOOK_PREUNINSTALL - ExecWait 'sc.exe stop PortmasterCore' - ExecWait 'sc.exe delete PortmasterCore' + ExecWait 'sc.exe stop PortmasterCore' $0 + ; Ignore errors if the service is not running + ExecWait 'sc.exe delete PortmasterCore' $0 + ; Ignore errors if the service does not exist !macroend diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 1ffe2abc..64ffc2a1 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -120,9 +120,12 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { defer func() { _ = file.Close() }() providedHash, err := hex.DecodeString(artifact.SHA256) - if err != nil || len(providedHash) != sha256.Size { + if err != nil { return false, fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) } + if len(providedHash) != sha256.Size { + return false, fmt.Errorf("invalid hash length for %s", artifact.SHA256) + } // Calculate hash of the file fileHash := sha256.New() diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 53ad2d8d..130265be 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -181,7 +181,7 @@ func (d *Downloader) downloadAndVerify(ctx context.Context) error { // Make sure we have the bundle file parsed. err := d.parseBundle() if err != nil { - log.Errorf("updates: invalid update bundle file: %s", err) + return fmt.Errorf("invalid update bundle file: %w", err) } // Make sure dir exists diff --git a/spn/navigator/module_test.go b/spn/navigator/module_test.go index c0de9194..d02e1e5d 100644 --- a/spn/navigator/module_test.go +++ b/spn/navigator/module_test.go @@ -24,7 +24,7 @@ type testInstance struct { geoip *geoip.GeoIP } -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -78,7 +78,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Intel Test", updates.UpdateIndex{}) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } From a874ec9412fd0da931e1f172a1c416fd2835e8cb Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 8 Oct 2024 14:13:08 +0300 Subject: [PATCH 30/62] [WIP] Fix unit tests --- cmds/hub/main.go | 5 +- cmds/notifier/.gitignore | 34 -- cmds/notifier/README.md | 5 - cmds/notifier/http_api.go | 63 --- cmds/notifier/icons.go | 25 - cmds/notifier/main.go | 287 ----------- cmds/notifier/notification.go | 35 -- cmds/notifier/notify.go | 102 ---- cmds/notifier/notify_linux.go | 160 ------ cmds/notifier/notify_windows.go | 184 ------- cmds/notifier/shutdown.go | 50 -- cmds/notifier/snoretoast-guid.patch | 15 - cmds/notifier/spn.go | 104 ---- cmds/notifier/subsystems.go | 121 ----- cmds/notifier/tray.go | 217 -------- .../notifier/wintoast/notification_builder.go | 90 ---- cmds/notifier/wintoast/wintoast.go | 217 -------- cmds/observation-hub/main.go | 2 - cmds/portmaster-start/.gitignore | 6 - cmds/portmaster-start/build | 77 --- cmds/portmaster-start/console_default.go | 11 - cmds/portmaster-start/console_windows.go | 150 ------ cmds/portmaster-start/dirs.go | 42 -- cmds/portmaster-start/install_windows.go | 180 ------- cmds/portmaster-start/lock.go | 109 ---- cmds/portmaster-start/logs.go | 127 ----- cmds/portmaster-start/main.go | 257 --------- cmds/portmaster-start/pack | 123 ----- cmds/portmaster-start/recover_linux.go | 82 --- cmds/portmaster-start/run.go | 486 ------------------ cmds/portmaster-start/service_windows.go | 134 ----- cmds/portmaster-start/show.go | 45 -- cmds/portmaster-start/shutdown.go | 49 -- cmds/portmaster-start/update.go | 158 ------ cmds/portmaster-start/verify.go | 179 ------- cmds/portmaster-start/version.go | 81 --- service/intel/geoip/init_test.go | 16 +- service/netenv/init_test.go | 16 +- service/profile/endpoints/endpoints_test.go | 17 +- service/resolver/main_test.go | 19 +- service/updates/assets/portmaster.service | 44 -- service/updates/bundlegeneration.go | 48 ++ service/updates/downloader.go | 16 +- service/updates/module.go | 4 - service/updates/updates_test.go | 89 ++++ spn/hub/hub_test.go | 17 +- spn/instance.go | 39 +- spn/navigator/module_test.go | 15 +- 48 files changed, 264 insertions(+), 4088 deletions(-) delete mode 100644 cmds/notifier/.gitignore delete mode 100644 cmds/notifier/README.md delete mode 100644 cmds/notifier/http_api.go delete mode 100644 cmds/notifier/icons.go delete mode 100644 cmds/notifier/main.go delete mode 100644 cmds/notifier/notification.go delete mode 100644 cmds/notifier/notify.go delete mode 100644 cmds/notifier/notify_linux.go delete mode 100644 cmds/notifier/notify_windows.go delete mode 100644 cmds/notifier/shutdown.go delete mode 100644 cmds/notifier/snoretoast-guid.patch delete mode 100644 cmds/notifier/spn.go delete mode 100644 cmds/notifier/subsystems.go delete mode 100644 cmds/notifier/tray.go delete mode 100644 cmds/notifier/wintoast/notification_builder.go delete mode 100644 cmds/notifier/wintoast/wintoast.go delete mode 100644 cmds/portmaster-start/.gitignore delete mode 100755 cmds/portmaster-start/build delete mode 100644 cmds/portmaster-start/console_default.go delete mode 100644 cmds/portmaster-start/console_windows.go delete mode 100644 cmds/portmaster-start/dirs.go delete mode 100644 cmds/portmaster-start/install_windows.go delete mode 100644 cmds/portmaster-start/lock.go delete mode 100644 cmds/portmaster-start/logs.go delete mode 100644 cmds/portmaster-start/main.go delete mode 100755 cmds/portmaster-start/pack delete mode 100644 cmds/portmaster-start/recover_linux.go delete mode 100644 cmds/portmaster-start/run.go delete mode 100644 cmds/portmaster-start/service_windows.go delete mode 100644 cmds/portmaster-start/show.go delete mode 100644 cmds/portmaster-start/shutdown.go delete mode 100644 cmds/portmaster-start/update.go delete mode 100644 cmds/portmaster-start/verify.go delete mode 100644 cmds/portmaster-start/version.go delete mode 100644 service/updates/assets/portmaster.service create mode 100644 service/updates/updates_test.go diff --git a/cmds/hub/main.go b/cmds/hub/main.go index 3db002b3..b0b6e1d1 100644 --- a/cmds/hub/main.go +++ b/cmds/hub/main.go @@ -18,13 +18,12 @@ import ( "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/service/updates/helper" "github.com/safing/portmaster/spn" "github.com/safing/portmaster/spn/conf" ) func init() { - flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") + // flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") } var sigUSR1 = syscall.Signal(0xa) @@ -40,7 +39,7 @@ func main() { // Configure user agent and updates. updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH) - helper.IntelOnly() + // helper.IntelOnly() // Set SPN public hub mode. conf.EnablePublicHub(true) diff --git a/cmds/notifier/.gitignore b/cmds/notifier/.gitignore deleted file mode 100644 index 602ad23c..00000000 --- a/cmds/notifier/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Compiled binaries -notifier -notifier.exe - -# Go vendor -vendor - -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out diff --git a/cmds/notifier/README.md b/cmds/notifier/README.md deleted file mode 100644 index bdfcece8..00000000 --- a/cmds/notifier/README.md +++ /dev/null @@ -1,5 +0,0 @@ -### Development Dependencies - -sudo apt install libgtk-3-dev libayatana-appindicator3-dev libwebkitgtk-3.0-dev libgl1-mesa-dev libglu1-mesa-dev libnotify-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev - -sudo pacman -S libappindicator-gtk3 diff --git a/cmds/notifier/http_api.go b/cmds/notifier/http_api.go deleted file mode 100644 index 7a68349a..00000000 --- a/cmds/notifier/http_api.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "strings" - "time" - - "github.com/safing/portmaster/base/log" -) - -const ( - apiBaseURL = "http://127.0.0.1:817/api/v1/" - apiShutdownEndpoint = "core/shutdown" -) - -var httpAPIClient *http.Client - -func init() { - // Make cookie jar. - jar, err := cookiejar.New(nil) - if err != nil { - log.Warningf("http-api: failed to create cookie jar: %s", err) - jar = nil - } - - // Create client. - httpAPIClient = &http.Client{ - Jar: jar, - Timeout: 3 * time.Second, - } -} - -func httpAPIAction(endpoint string) (response string, err error) { - // Make action request. - resp, err := httpAPIClient.Post(apiBaseURL+endpoint, "", nil) - if err != nil { - return "", fmt.Errorf("request failed: %w", err) - } - - // Read the response body. - defer func() { _ = resp.Body.Close() }() - respData, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read data: %w", err) - } - response = strings.TrimSpace(string(respData)) - - // Check if the request was successful on the server. - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return response, fmt.Errorf("server failed with %s: %s", resp.Status, response) - } - - return response, nil -} - -// TriggerShutdown triggers a shutdown via the APi. -func TriggerShutdown() error { - _, err := httpAPIAction(apiShutdownEndpoint) - return err -} diff --git a/cmds/notifier/icons.go b/cmds/notifier/icons.go deleted file mode 100644 index b3690a3f..00000000 --- a/cmds/notifier/icons.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "sync" - - icons "github.com/safing/portmaster/assets" -) - -var ( - appIconEnsureOnce sync.Once - appIconPath string -) - -func ensureAppIcon() (location string, err error) { - appIconEnsureOnce.Do(func() { - if appIconPath == "" { - appIconPath = filepath.Join(dataDir, "exec", "portmaster.png") - } - err = os.WriteFile(appIconPath, icons.PNG, 0o0644) // nolint:gosec - }) - - return appIconPath, err -} diff --git a/cmds/notifier/main.go b/cmds/notifier/main.go deleted file mode 100644 index e40487bb..00000000 --- a/cmds/notifier/main.go +++ /dev/null @@ -1,287 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "fmt" - "os" - "os/signal" - "path/filepath" - "runtime" - "runtime/pprof" - "strings" - "sync" - "syscall" - "time" - - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - dataDir string - printStackOnExit bool - showVersion bool - - apiClient = client.NewClient("127.0.0.1:817") - connected = abool.New() - shuttingDown = abool.New() - restarting = abool.New() - - mainCtx, cancelMainCtx = context.WithCancel(context.Background()) - mainWg = &sync.WaitGroup{} - - dataRoot *utils.DirStructure - // Create registry. - registry = &updater.ResourceRegistry{ - Name: "updates", - UpdateURLs: []string{ - "https://updates.safing.io", - }, - DevMode: false, - Online: false, // disable download of resources (this is job for the core). - } -) - -const query = "query " - -func init() { - flag.StringVar(&dataDir, "data", "", "set data directory") - flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") - flag.BoolVar(&showVersion, "version", false, "show version and exit") - - runtime.GOMAXPROCS(2) -} - -func main() { - // parse flags - flag.Parse() - - // set meta info - info.Set("Portmaster Notifier", "0.3.6", "GPLv3") - - // check if meta info is ok - err := info.CheckVersion() - if err != nil { - fmt.Println("compile error: please compile using the provided build script") - os.Exit(1) - } - - // print help - // if modules.HelpFlag { - // flag.Usage() - // os.Exit(0) - // } - - if showVersion { - fmt.Println(info.FullVersion()) - os.Exit(0) - } - - // auto detect - if dataDir == "" { - dataDir = detectDataDir() - } - - // check data dir - if dataDir == "" { - fmt.Fprintln(os.Stderr, "please set the data directory using --data=/path/to/data/dir") - os.Exit(1) - } - - // switch to safe exec dir - err = os.Chdir(filepath.Join(dataDir, "exec")) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: failed to switch to safe exec dir: %s\n", err) - } - - // start log writer - err = log.Start() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to start logging: %s\n", err) - os.Exit(1) - } - - // load registry - err = configureRegistry(true) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to load registry: %s\n", err) - os.Exit(1) - } - - // connect to API - go apiClient.StayConnected() - go apiStatusMonitor() - - // start subsystems - go tray() - go subsystemsClient() - go spnStatusClient() - go notifClient() - go startShutdownEventListener() - - // Shutdown - // catch interrupt for clean shutdown - signalCh := make(chan os.Signal, 1) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // wait for shutdown - select { - case <-signalCh: - fmt.Println(" ") - log.Warning("program was interrupted, shutting down") - case <-mainCtx.Done(): - log.Warning("program is shutting down") - } - - if printStackOnExit { - fmt.Println("=== PRINTING STACK ===") - _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) - fmt.Println("=== END STACK ===") - } - go func() { - time.Sleep(10 * time.Second) - fmt.Println("===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====") - _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) - os.Exit(1) - }() - - // clear all notifications - clearNotifications() - - // shutdown - cancelMainCtx() - mainWg.Wait() - - apiClient.Shutdown() - exitTray() - log.Shutdown() - - os.Exit(0) -} - -func apiStatusMonitor() { - for { - // Wait for connection. - <-apiClient.Online() - connected.Set() - triggerTrayUpdate() - - // Wait for lost connection. - <-apiClient.Offline() - connected.UnSet() - triggerTrayUpdate() - } -} - -func detectDataDir() string { - // get path of executable - binPath, err := os.Executable() - if err != nil { - return "" - } - // get directory - binDir := filepath.Dir(binPath) - // check if we in the updates directory - identifierDir := filepath.Join("updates", runtime.GOOS+"_"+runtime.GOARCH, "notifier") - // check if there is a match and return data dir - if strings.HasSuffix(binDir, identifierDir) { - return filepath.Clean(strings.TrimSuffix(binDir, identifierDir)) - } - return "" -} - -func configureRegistry(mustLoadIndex bool) error { - // If dataDir is not set, check the environment variable. - if dataDir == "" { - dataDir = os.Getenv("PORTMASTER_DATA") - } - - // If it's still empty, try to auto-detect it. - if dataDir == "" { - dataDir = detectInstallationDir() - } - - // Finally, if it's still empty, the user must provide it. - if dataDir == "" { - return errors.New("please set the data directory using --data=/path/to/data/dir") - } - - // Remove left over quotes. - dataDir = strings.Trim(dataDir, `\"`) - // Initialize data root. - err := dataroot.Initialize(dataDir, 0o0755) - if err != nil { - return fmt.Errorf("failed to initialize data root: %w", err) - } - dataRoot = dataroot.Root() - - // Initialize registry. - err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755)) - if err != nil { - return err - } - - return updateRegistryIndex(mustLoadIndex) -} - -func detectInstallationDir() string { - exePath, err := filepath.Abs(os.Args[0]) - if err != nil { - return "" - } - - parent := filepath.Dir(exePath) // parent should be "...\updates\windows_amd64\notifier" - stableJSONFile := filepath.Join(parent, "..", "..", "stable.json") // "...\updates\stable.json" - stat, err := os.Stat(stableJSONFile) - if err != nil { - return "" - } - - if stat.IsDir() { - return "" - } - - return parent -} - -func updateRegistryIndex(mustLoadIndex bool) error { - // Set indexes based on the release channel. - warning := helper.SetIndexes(registry, "", false, false, false) - if warning != nil { - log.Warningf("%q", warning) - } - - // Load indexes from disk or network, if needed and desired. - err := registry.LoadIndexes(context.Background()) - if err != nil { - log.Warningf("error loading indexes %q", warning) - if mustLoadIndex { - return err - } - } - - // Load versions from disk to know which others we have and which are available. - err = registry.ScanStorage("") - if err != nil { - log.Warningf("error during storage scan: %q\n", err) - } - - registry.SelectVersions() - return nil -} diff --git a/cmds/notifier/notification.go b/cmds/notifier/notification.go deleted file mode 100644 index 0f6ded8d..00000000 --- a/cmds/notifier/notification.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - - pbnotify "github.com/safing/portmaster/base/notifications" -) - -// Notification represents a notification that is to be delivered to the user. -type Notification struct { - pbnotify.Notification - - // systemID holds the ID returned by the dbus interface on Linux or by WinToast library on Windows. - systemID NotificationID -} - -// IsSupportedAction returns whether the action is supported on this system. -func IsSupportedAction(a pbnotify.Action) bool { - switch a.Type { - case pbnotify.ActionTypeNone: - return true - default: - return false - } -} - -// SelectAction sends an action back to the portmaster. -func (n *Notification) SelectAction(action string) { - upd := &pbnotify.Notification{ - EventID: n.EventID, - SelectedActionID: action, - } - - _ = apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, upd.EventID), upd, nil) -} diff --git a/cmds/notifier/notify.go b/cmds/notifier/notify.go deleted file mode 100644 index 48e117c0..00000000 --- a/cmds/notifier/notify.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" - pbnotify "github.com/safing/portmaster/base/notifications" - "github.com/safing/structures/dsd" -) - -const ( - dbNotifBasePath = "notifications:all/" -) - -var ( - notifications = make(map[string]*Notification) - notificationsLock sync.Mutex -) - -func notifClient() { - notifOp := apiClient.Qsub(fmt.Sprintf("query %s where ShowOnSystem is true", dbNotifBasePath), handleNotification) - notifOp.EnableResuscitation() - - // start the action listener and block - // until it's closed. - actionListener() -} - -func handleNotification(m *client.Message) { - notificationsLock.Lock() - defer notificationsLock.Unlock() - - log.Tracef("received %s msg: %s", m.Type, m.Key) - - switch m.Type { - case client.MsgError: - case client.MsgDone: - case client.MsgSuccess: - case client.MsgOk, client.MsgUpdate, client.MsgNew: - - n := &Notification{} - _, err := dsd.Load(m.RawValue, &n.Notification) - if err != nil { - log.Warningf("notify: failed to parse new notification: %s", err) - return - } - - // copy existing system values - existing, ok := notifications[n.EventID] - if ok { - existing.Lock() - n.systemID = existing.systemID - existing.Unlock() - } - - // save - notifications[n.EventID] = n - - // Handle notification. - switch { - case existing != nil: - // Cancel existing notification if not active, else ignore. - if n.State != pbnotify.Active { - existing.Cancel() - } - return - case n.State == pbnotify.Active: - // Show new notifications that are active. - n.Show() - default: - // Ignore new notifications that are not active. - } - - case client.MsgDelete: - - n, ok := notifications[strings.TrimPrefix(m.Key, dbNotifBasePath)] - if ok { - n.Cancel() - delete(notifications, n.EventID) - } - - case client.MsgWarning: - case client.MsgOffline: - } -} - -func clearNotifications() { - notificationsLock.Lock() - defer notificationsLock.Unlock() - - for _, n := range notifications { - n.Cancel() - } - - // Wait for goroutines that cancel notifications. - // TODO: Revamp to use a waitgroup. - time.Sleep(1 * time.Second) -} diff --git a/cmds/notifier/notify_linux.go b/cmds/notifier/notify_linux.go deleted file mode 100644 index 80cc8e15..00000000 --- a/cmds/notifier/notify_linux.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -import ( - "context" - "errors" - "sync" - - notify "github.com/dhaavi/go-notify" - - "github.com/safing/portmaster/base/log" -) - -type NotificationID uint32 - -var ( - capabilities notify.Capabilities - notifsByID sync.Map -) - -func init() { - var err error - capabilities, err = notify.GetCapabilities() - if err != nil { - log.Errorf("failed to get notification system capabilities: %s", err) - } -} - -func handleActions(ctx context.Context, actions chan notify.Signal) { - mainWg.Add(1) - defer mainWg.Done() - -listenForNotifications: - for { - select { - case <-ctx.Done(): - return - case sig := <-actions: - if sig.Name != "org.freedesktop.Notifications.ActionInvoked" { - // we don't care for anything else (dismissed, closed) - continue listenForNotifications - } - - // get notification by system ID - n, ok := notifsByID.LoadAndDelete(NotificationID(sig.ID)) - - if !ok { - continue listenForNotifications - } - - notification, ok := n.(*Notification) - if !ok { - log.Errorf("received invalid notification type %T", n) - - continue listenForNotifications - } - - log.Tracef("notify: received signal: %+v", sig) - if sig.ActionKey != "" { - // send action - if ok { - notification.Lock() - notification.SelectAction(sig.ActionKey) - notification.Unlock() - } - } else { - log.Tracef("notify: notification clicked: %+v", sig) - // Global action invoked, start the app - launchApp() - } - } - } -} - -func actionListener() { - actions := make(chan notify.Signal, 100) - - go handleActions(mainCtx, actions) - - err := notify.SignalNotify(mainCtx, actions) - if err != nil && errors.Is(err, context.Canceled) { - log.Errorf("notify: signal listener failed: %s", err) - } -} - -// Show shows the notification. -func (n *Notification) Show() { - sysN := notify.NewNotification("Portmaster", n.Message) - // see https://developer.gnome.org/notification-spec/ - - // The optional name of the application sending the notification. - // Can be blank. - sysN.AppName = "Portmaster" - - // The optional notification ID that this notification replaces. - sysN.ReplacesID = uint32(n.systemID) - - // The optional program icon of the calling application. - // sysN.AppIcon string - - // The summary text briefly describing the notification. - // Summary string (arg 1) - - // The optional detailed body text. - // Body string (arg 2) - - // The actions send a request message back to the notification client - // when invoked. - // sysN.Actions []string - if capabilities.Actions { - sysN.Actions = make([]string, 0, len(n.AvailableActions)*2) - for _, action := range n.AvailableActions { - if IsSupportedAction(*action) { - sysN.Actions = append(sysN.Actions, action.ID) - sysN.Actions = append(sysN.Actions, action.Text) - } - } - } - - // Set Portmaster icon. - iconLocation, err := ensureAppIcon() - if err != nil { - log.Warningf("notify: failed to write icon: %s", err) - } - sysN.AppIcon = iconLocation - - // TODO: Use hints to display icon of affected app. - // Hints are a way to provide extra data to a notification server. - // sysN.Hints = make(map[string]interface{}) - - // The timeout time in milliseconds since the display of the - // notification at which the notification should automatically close. - // sysN.Timeout int32 - - newID, err := sysN.Show() - if err != nil { - log.Warningf("notify: failed to show notification %s", n.EventID) - return - } - - notifsByID.Store(NotificationID(newID), n) - - n.Lock() - defer n.Unlock() - n.systemID = NotificationID(newID) -} - -// Cancel cancels the notification. -func (n *Notification) Cancel() { - n.Lock() - defer n.Unlock() - - // TODO: could a ID of 0 be valid? - if n.systemID != 0 { - err := notify.CloseNotification(uint32(n.systemID)) - if err != nil { - log.Warningf("notify: failed to close notification %s/%d", n.EventID, n.systemID) - } - notifsByID.Delete(n.systemID) - } -} diff --git a/cmds/notifier/notify_windows.go b/cmds/notifier/notify_windows.go deleted file mode 100644 index 98cf987a..00000000 --- a/cmds/notifier/notify_windows.go +++ /dev/null @@ -1,184 +0,0 @@ -package main - -import ( - "fmt" - "sync" - - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/cmds/notifier/wintoast" - "github.com/safing/portmaster/service/updates/helper" -) - -type NotificationID int64 - -const ( - appName = "Portmaster" - appUserModelID = "io.safing.portmaster.2" - originalShortcutPath = "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Portmaster\\Portmaster.lnk" -) - -const ( - SoundDefault = 0 - SoundSilent = 1 - SoundLoop = 2 -) - -const ( - SoundPathDefault = 0 - // see notification_glue.h if you need more types -) - -var ( - initOnce sync.Once - lib *wintoast.WinToast - notificationsByIDs sync.Map -) - -func getLib() *wintoast.WinToast { - initOnce.Do(func() { - dllPath, err := getDllPath() - if err != nil { - log.Errorf("notify: failed to get dll path: %s", err) - return - } - // Load dll and all the functions - newLib, err := wintoast.New(dllPath) - if err != nil { - log.Errorf("notify: failed to load library: %s", err) - return - } - - // Initialize. This will create or update application shortcut. C:\Users\\AppData\Roaming\Microsoft\Windows\Start Menu\Programs - // and it will be of the originalShortcutPath with no CLSID and different AUMI - err = newLib.Initialize(appName, appUserModelID, originalShortcutPath) - if err != nil { - log.Errorf("notify: failed to load library: %s", err) - return - } - - // library was initialized successfully - lib = newLib - - // Set callbacks - - err = lib.SetCallbacks(notificationActivatedCallback, notificationDismissedCallback, notificationDismissedCallback) - if err != nil { - log.Warningf("notify: failed to set callbacks: %s", err) - return - } - }) - - return lib -} - -// Show shows the notification. -func (n *Notification) Show() { - // Lock notification - n.Lock() - defer n.Unlock() - - // Create new notification object - builder, err := getLib().NewNotification(n.Title, n.Message) - if err != nil { - log.Errorf("notify: failed to create notification: %s", err) - return - } - // Make sure memory is freed when done - defer builder.Delete() - - // if needed set notification icon - // _ = builder.SetImage(iconLocation) - - // Leaving the default value for the sound - // _ = builder.SetSound(SoundDefault, SoundPathDefault) - - // Set all the required actions. - for _, action := range n.AvailableActions { - err = builder.AddButton(action.Text) - if err != nil { - log.Warningf("notify: failed to add button: %s", err) - } - } - - // Show notification. - id, err := builder.Show() - if err != nil { - log.Errorf("notify: failed to show notification: %s", err) - return - } - n.systemID = NotificationID(id) - - // Link system id to the notification object - notificationsByIDs.Store(NotificationID(id), n) - - log.Debugf("notify: showing notification %q: %d", n.Title, n.systemID) -} - -// Cancel cancels the notification. -func (n *Notification) Cancel() { - // Lock notification - n.Lock() - defer n.Unlock() - - // No need to check for errors. If it fails it is probably already dismissed - _ = getLib().HideNotification(int64(n.systemID)) - - notificationsByIDs.Delete(n.systemID) - log.Debugf("notify: notification canceled %q: %d", n.Title, n.systemID) -} - -func notificationActivatedCallback(id int64, actionIndex int32) { - if actionIndex == -1 { - // The user clicked on the notification (not a button), open the portmaster and delete - launchApp() - notificationsByIDs.Delete(NotificationID(id)) - log.Debugf("notify: notification clicked %d", id) - return - } - - // The user click one of the buttons - - // Get notified object - n, ok := notificationsByIDs.LoadAndDelete(NotificationID(id)) - if !ok { - return - } - - notification := n.(*Notification) - - notification.Lock() - defer notification.Unlock() - - // Set selected action - actionID := notification.AvailableActions[actionIndex].ID - notification.SelectAction(actionID) - - log.Debugf("notify: notification button cliecked %d button id: %d", id, actionIndex) -} - -func notificationDismissedCallback(id int64, reason int32) { - // Failure or user dismissed the notification - if reason == 0 { - notificationsByIDs.Delete(NotificationID(id)) - log.Debugf("notify: notification dissmissed %d", id) - } -} - -func getDllPath() (string, error) { - if dataDir == "" { - return "", fmt.Errorf("dataDir is empty") - } - - // Aks the registry for the dll path - identifier := helper.PlatformIdentifier("notifier/portmaster-wintoast.dll") - file, err := registry.GetFile(identifier) - if err != nil { - return "", err - } - return file.Path(), nil -} - -func actionListener() { - // initialize the library - _ = getLib() -} diff --git a/cmds/notifier/shutdown.go b/cmds/notifier/shutdown.go deleted file mode 100644 index 70b2e6d8..00000000 --- a/cmds/notifier/shutdown.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" -) - -func startShutdownEventListener() { - shutdownNotifOp := apiClient.Sub("query runtime:modules/core/event/shutdown", handleShutdownEvent) - shutdownNotifOp.EnableResuscitation() - - restartNotifOp := apiClient.Sub("query runtime:modules/core/event/restart", handleRestartEvent) - restartNotifOp.EnableResuscitation() -} - -func handleShutdownEvent(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - shuttingDown.Set() - triggerTrayUpdate() - - log.Warningf("shutdown: received shutdown event, shutting down now") - - // wait for the API client connection to die - <-apiClient.Offline() - shuttingDown.UnSet() - - cancelMainCtx() - - case client.MsgWarning, client.MsgError: - log.Errorf("shutdown: event subscription error: %s", string(m.RawValue)) - } -} - -func handleRestartEvent(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - restarting.Set() - triggerTrayUpdate() - - log.Warningf("restart: received restart event") - - // wait for the API client connection to die - <-apiClient.Offline() - restarting.UnSet() - triggerTrayUpdate() - case client.MsgWarning, client.MsgError: - log.Errorf("shutdown: event subscription error: %s", string(m.RawValue)) - } -} diff --git a/cmds/notifier/snoretoast-guid.patch b/cmds/notifier/snoretoast-guid.patch deleted file mode 100644 index 1a050e5f..00000000 --- a/cmds/notifier/snoretoast-guid.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 498226a..446ba5e 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.4) - - project(snoretoast VERSION 0.6.0) - # Always change the guid when the version is changed SNORETOAST_CALLBACK_GUID --set(SNORETOAST_CALLBACK_GUID eb1fdd5b-8f70-4b5a-b230-998a2dc19303) -+#We keep it fixed! -+set(SNORETOAST_CALLBACK_GUID 7F00FB48-65D5-4BA8-A35B-F194DA7E1A51) -+ - - set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/) - diff --git a/cmds/notifier/spn.go b/cmds/notifier/spn.go deleted file mode 100644 index 0b49d63c..00000000 --- a/cmds/notifier/spn.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "sync" - "time" - - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" - "github.com/safing/structures/dsd" -) - -const ( - spnModuleKey = "config:spn/enable" - spnStatusKey = "runtime:spn/status" -) - -var ( - spnEnabled = abool.New() - - spnStatusCache *SPNStatus - spnStatusCacheLock sync.Mutex -) - -// SPNStatus holds SPN status information. -type SPNStatus struct { - Status string - HomeHubID string - HomeHubName string - ConnectedIP string - ConnectedTransport string - ConnectedSince *time.Time -} - -// GetSPNStatus returns the SPN status. -func GetSPNStatus() *SPNStatus { - spnStatusCacheLock.Lock() - defer spnStatusCacheLock.Unlock() - - return spnStatusCache -} - -func updateSPNStatus(s *SPNStatus) { - spnStatusCacheLock.Lock() - defer spnStatusCacheLock.Unlock() - - spnStatusCache = s -} - -func spnStatusClient() { - moduleQueryOp := apiClient.Qsub(query+spnModuleKey, handleSPNModuleUpdate) - moduleQueryOp.EnableResuscitation() - - statusQueryOp := apiClient.Qsub(query+spnStatusKey, handleSPNStatusUpdate) - statusQueryOp.EnableResuscitation() -} - -func handleSPNModuleUpdate(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - var cfg struct { - Value bool `json:"Value"` - } - _, err := dsd.Load(m.RawValue, &cfg) - if err != nil { - log.Warningf("config: failed to parse config: %s", err) - return - } - log.Infof("config: received update to SPN module: enabled=%v", cfg.Value) - - spnEnabled.SetTo(cfg.Value) - triggerTrayUpdate() - - default: - } -} - -func handleSPNStatusUpdate(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - newStatus := &SPNStatus{} - _, err := dsd.Load(m.RawValue, newStatus) - if err != nil { - log.Warningf("config: failed to parse config: %s", err) - return - } - log.Infof("config: received update to SPN status: %+v", newStatus) - - updateSPNStatus(newStatus) - triggerTrayUpdate() - - default: - } -} - -func ToggleSPN() { - var cfg struct { - Value bool `json:"Value"` - } - cfg.Value = !spnEnabled.IsSet() - - apiClient.Update(spnModuleKey, &cfg, nil) -} diff --git a/cmds/notifier/subsystems.go b/cmds/notifier/subsystems.go deleted file mode 100644 index 587d6d84..00000000 --- a/cmds/notifier/subsystems.go +++ /dev/null @@ -1,121 +0,0 @@ -package main - -import ( - "sync" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" - "github.com/safing/structures/dsd" -) - -const ( - subsystemsKeySpace = "runtime:subsystems/" - - // Module Failure Status Values - // FailureNone = 0 // unused - // FailureHint = 1 // unused. - FailureWarning = 2 - FailureError = 3 -) - -var ( - subsystems = make(map[string]*Subsystem) - subsystemsLock sync.Mutex -) - -// Subsystem describes a subset of modules that represent a part of a -// service or program to the user. Subsystems can be (de-)activated causing -// all related modules to be brought down or up. -type Subsystem struct { //nolint:maligned // not worth the effort - // ID is a unique identifier for the subsystem. - ID string - - // Name holds a human readable name of the subsystem. - Name string - - // Description may holds an optional description of - // the subsystem's purpose. - Description string - - // Modules contains all modules that are related to the subsystem. - // Note that this slice also contains a reference to the subsystem - // module itself. - Modules []*ModuleStatus - - // FailureStatus is the worst failure status that is currently - // set in one of the subsystem's dependencies. - FailureStatus uint8 -} - -// ModuleStatus describes the status of a module. -type ModuleStatus struct { - Name string - Enabled bool - Status uint8 - FailureStatus uint8 - FailureID string - FailureMsg string -} - -// GetFailure returns the worst of all subsystem failures. -func GetFailure() (failureStatus uint8, failureMsg string) { - subsystemsLock.Lock() - defer subsystemsLock.Unlock() - - for _, subsystem := range subsystems { - for _, module := range subsystem.Modules { - if failureStatus < module.FailureStatus { - failureStatus = module.FailureStatus - failureMsg = module.FailureMsg - } - } - } - - return -} - -func updateSubsystem(s *Subsystem) { - subsystemsLock.Lock() - defer subsystemsLock.Unlock() - - subsystems[s.ID] = s -} - -func clearSubsystems() { - subsystemsLock.Lock() - defer subsystemsLock.Unlock() - - for key := range subsystems { - delete(subsystems, key) - } -} - -func subsystemsClient() { - subsystemsOp := apiClient.Qsub("query "+subsystemsKeySpace, handleSubsystem) - subsystemsOp.EnableResuscitation() -} - -func handleSubsystem(m *client.Message) { - switch m.Type { - case client.MsgError: - case client.MsgDone: - case client.MsgSuccess: - case client.MsgOk, client.MsgUpdate, client.MsgNew: - - newSubsystem := &Subsystem{} - _, err := dsd.Load(m.RawValue, newSubsystem) - if err != nil { - log.Warningf("subsystems: failed to parse new subsystem: %s", err) - return - } - updateSubsystem(newSubsystem) - triggerTrayUpdate() - - case client.MsgDelete: - case client.MsgWarning: - case client.MsgOffline: - - clearSubsystems() - - } -} diff --git a/cmds/notifier/tray.go b/cmds/notifier/tray.go deleted file mode 100644 index abdf48d5..00000000 --- a/cmds/notifier/tray.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -import ( - "flag" - "os/exec" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - "fyne.io/systray" - - icons "github.com/safing/portmaster/assets" - "github.com/safing/portmaster/base/log" -) - -const ( - shortenStatusMsgTo = 40 -) - -var ( - trayLock sync.Mutex - - scaleColoredIconsTo int - - activeIconID int = -1 - activeStatusMsg = "" - activeSPNStatus = "" - activeSPNSwitch = "" - - menuItemStatusMsg *systray.MenuItem - menuItemSPNStatus *systray.MenuItem - menuItemSPNSwitch *systray.MenuItem -) - -func init() { - flag.IntVar(&scaleColoredIconsTo, "scale-icons", 32, "scale colored icons to given size in pixels") - - // lock until ready - trayLock.Lock() -} - -func tray() { - if scaleColoredIconsTo > 0 { - icons.ScaleColoredIconsTo(scaleColoredIconsTo) - } - - systray.Run(onReady, onExit) -} - -func exitTray() { - systray.Quit() -} - -func onReady() { - // unlock when ready - defer trayLock.Unlock() - - // icon - systray.SetIcon(icons.ColoredIcons[icons.RedID]) - if runtime.GOOS == "windows" { - // systray.SetTitle("Portmaster Notifier") // Don't set title, as it may be displayed in full in the menu/tray bar. (Ubuntu) - systray.SetTooltip("Portmaster Notifier") - } - - // menu: open app - if dataDir != "" { - menuItemOpenApp := systray.AddMenuItem("Open App", "") - go clickListener(menuItemOpenApp, launchApp) - systray.AddSeparator() - } - - // menu: status - - menuItemStatusMsg = systray.AddMenuItem("Loading...", "") - menuItemStatusMsg.Disable() - systray.AddSeparator() - - // menu: SPN - - menuItemSPNStatus = systray.AddMenuItem("Loading...", "") - menuItemSPNStatus.Disable() - menuItemSPNSwitch = systray.AddMenuItem("Loading...", "") - go clickListener(menuItemSPNSwitch, func() { - ToggleSPN() - }) - systray.AddSeparator() - - // menu: quit - systray.AddSeparator() - closeTray := systray.AddMenuItem("Close Tray Notifier", "") - go clickListener(closeTray, func() { - cancelMainCtx() - }) - shutdownPortmaster := systray.AddMenuItem("Shut Down Portmaster", "") - go clickListener(shutdownPortmaster, func() { - _ = TriggerShutdown() - time.Sleep(1 * time.Second) - cancelMainCtx() - }) -} - -func onExit() { -} - -func triggerTrayUpdate() { - // TODO: Deduplicate triggers. - go updateTray() -} - -// updateTray update the state of the tray depending on the currently available information. -func updateTray() { - // Get current information. - spnStatus := GetSPNStatus() - failureID, failureMsg := GetFailure() - - trayLock.Lock() - defer trayLock.Unlock() - - // Select icon and status message to show. - newIconID := icons.GreenID - newStatusMsg := "Secure" - switch { - case shuttingDown.IsSet(): - newIconID = icons.RedID - newStatusMsg = "Shutting Down Portmaster" - - case restarting.IsSet(): - newIconID = icons.YellowID - newStatusMsg = "Restarting Portmaster" - - case !connected.IsSet(): - newIconID = icons.RedID - newStatusMsg = "Waiting for Portmaster Core Service" - - case failureID == FailureError: - newIconID = icons.RedID - newStatusMsg = failureMsg - - case failureID == FailureWarning: - newIconID = icons.YellowID - newStatusMsg = failureMsg - - case spnEnabled.IsSet(): - newIconID = icons.BlueID - } - - // Set icon if changed. - if newIconID != activeIconID { - activeIconID = newIconID - systray.SetIcon(icons.ColoredIcons[activeIconID]) - } - - // Set message if changed. - if newStatusMsg != activeStatusMsg { - activeStatusMsg = newStatusMsg - - // Shorten message if too long. - shortenedMsg := activeStatusMsg - if len(shortenedMsg) > shortenStatusMsgTo && strings.Contains(shortenedMsg, ". ") { - shortenedMsg = strings.SplitN(shortenedMsg, ". ", 2)[0] - } - if len(shortenedMsg) > shortenStatusMsgTo { - shortenedMsg = shortenedMsg[:shortenStatusMsgTo] + "..." - } - - menuItemStatusMsg.SetTitle("Status: " + shortenedMsg) - } - - // Set SPN status if changed. - if spnStatus != nil && activeSPNStatus != spnStatus.Status { - activeSPNStatus = spnStatus.Status - menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) // nolint:staticcheck - } - - // Set SPN switch if changed. - newSPNSwitch := "Enable SPN" - if spnEnabled.IsSet() { - newSPNSwitch = "Disable SPN" - } - if activeSPNSwitch != newSPNSwitch { - activeSPNSwitch = newSPNSwitch - menuItemSPNSwitch.SetTitle(activeSPNSwitch) - } -} - -func clickListener(item *systray.MenuItem, fn func()) { - for range item.ClickedCh { - fn() - } -} - -func launchApp() { - // build path to app - pmStartPath := filepath.Join(dataDir, "portmaster-start") - if runtime.GOOS == "windows" { - pmStartPath += ".exe" - } - - // start app - cmd := exec.Command(pmStartPath, "app", "--data", dataDir) - err := cmd.Start() - if err != nil { - log.Warningf("failed to start app: %s", err) - return - } - - // Use cmd.Wait() instead of cmd.Process.Release() to properly release its resources. - // See https://github.com/golang/go/issues/36534 - go func() { - err := cmd.Wait() - if err != nil { - log.Warningf("failed to wait/release app process: %s", err) - } - }() -} diff --git a/cmds/notifier/wintoast/notification_builder.go b/cmds/notifier/wintoast/notification_builder.go deleted file mode 100644 index 89eca798..00000000 --- a/cmds/notifier/wintoast/notification_builder.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build windows - -package wintoast - -import ( - "unsafe" - - "golang.org/x/sys/windows" -) - -type NotificationBuilder struct { - templatePointer uintptr - lib *WinToast -} - -func newNotification(lib *WinToast, title string, message string) (*NotificationBuilder, error) { - lib.Lock() - defer lib.Unlock() - - titleUTF, _ := windows.UTF16PtrFromString(title) - messageUTF, _ := windows.UTF16PtrFromString(message) - titleP := unsafe.Pointer(titleUTF) - messageP := unsafe.Pointer(messageUTF) - - ptr, _, err := lib.createNotification.Call(uintptr(titleP), uintptr(messageP)) - if ptr == 0 { - return nil, err - } - - return &NotificationBuilder{ptr, lib}, nil -} - -func (n *NotificationBuilder) Delete() { - if n == nil { - return - } - - n.lib.Lock() - defer n.lib.Unlock() - - _, _, _ = n.lib.deleteNotification.Call(n.templatePointer) -} - -func (n *NotificationBuilder) AddButton(text string) error { - n.lib.Lock() - defer n.lib.Unlock() - textUTF, _ := windows.UTF16PtrFromString(text) - textP := unsafe.Pointer(textUTF) - - rc, _, err := n.lib.addButton.Call(n.templatePointer, uintptr(textP)) - if rc != 1 { - return err - } - return nil -} - -func (n *NotificationBuilder) SetImage(iconPath string) error { - n.lib.Lock() - defer n.lib.Unlock() - pathUTF, _ := windows.UTF16PtrFromString(iconPath) - pathP := unsafe.Pointer(pathUTF) - - rc, _, err := n.lib.setImage.Call(n.templatePointer, uintptr(pathP)) - if rc != 1 { - return err - } - return nil -} - -func (n *NotificationBuilder) SetSound(option int, path int) error { - n.lib.Lock() - defer n.lib.Unlock() - - rc, _, err := n.lib.setSound.Call(n.templatePointer, uintptr(option), uintptr(path)) - if rc != 1 { - return err - } - return nil -} - -func (n *NotificationBuilder) Show() (int64, error) { - n.lib.Lock() - defer n.lib.Unlock() - - id, _, err := n.lib.showNotification.Call(n.templatePointer) - if int64(id) == -1 { - return -1, err - } - return int64(id), nil -} diff --git a/cmds/notifier/wintoast/wintoast.go b/cmds/notifier/wintoast/wintoast.go deleted file mode 100644 index 5d9a3380..00000000 --- a/cmds/notifier/wintoast/wintoast.go +++ /dev/null @@ -1,217 +0,0 @@ -//go:build windows - -package wintoast - -import ( - "fmt" - "sync" - "unsafe" - - "github.com/tevino/abool" - - "golang.org/x/sys/windows" -) - -// WinNotify holds the DLL handle. -type WinToast struct { - sync.RWMutex - - dll *windows.DLL - - initialized *abool.AtomicBool - - initialize *windows.Proc - isInitialized *windows.Proc - createNotification *windows.Proc - deleteNotification *windows.Proc - addButton *windows.Proc - setImage *windows.Proc - setSound *windows.Proc - showNotification *windows.Proc - hideNotification *windows.Proc - setActivatedCallback *windows.Proc - setDismissedCallback *windows.Proc - setFailedCallback *windows.Proc -} - -func New(dllPath string) (*WinToast, error) { - if dllPath == "" { - return nil, fmt.Errorf("winnotifiy: path to dll not specified") - } - - libraryObject := &WinToast{} - libraryObject.initialized = abool.New() - - // load dll - var err error - libraryObject.dll, err = windows.LoadDLL(dllPath) - if err != nil { - return nil, fmt.Errorf("winnotifiy: failed to load notifier dll %w", err) - } - - // load functions - libraryObject.initialize, err = libraryObject.dll.FindProc("PortmasterToastInitialize") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastInitialize not found %w", err) - } - - libraryObject.isInitialized, err = libraryObject.dll.FindProc("PortmasterToastIsInitialized") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastIsInitialized not found %w", err) - } - - libraryObject.createNotification, err = libraryObject.dll.FindProc("PortmasterToastCreateNotification") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastCreateNotification not found %w", err) - } - - libraryObject.deleteNotification, err = libraryObject.dll.FindProc("PortmasterToastDeleteNotification") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastDeleteNotification not found %w", err) - } - - libraryObject.addButton, err = libraryObject.dll.FindProc("PortmasterToastAddButton") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastAddButton not found %w", err) - } - - libraryObject.setImage, err = libraryObject.dll.FindProc("PortmasterToastSetImage") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastSetImage not found %w", err) - } - - libraryObject.setSound, err = libraryObject.dll.FindProc("PortmasterToastSetSound") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastSetSound not found %w", err) - } - - libraryObject.showNotification, err = libraryObject.dll.FindProc("PortmasterToastShow") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastShow not found %w", err) - } - - libraryObject.setActivatedCallback, err = libraryObject.dll.FindProc("PortmasterToastActivatedCallback") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterActivatedCallback not found %w", err) - } - - libraryObject.setDismissedCallback, err = libraryObject.dll.FindProc("PortmasterToastDismissedCallback") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastDismissedCallback not found %w", err) - } - - libraryObject.setFailedCallback, err = libraryObject.dll.FindProc("PortmasterToastFailedCallback") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastFailedCallback not found %w", err) - } - - libraryObject.hideNotification, err = libraryObject.dll.FindProc("PortmasterToastHide") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastHide not found %w", err) - } - - return libraryObject, nil -} - -func (lib *WinToast) Initialize(appName, aumi, originalShortcutPath string) error { - if lib == nil { - return fmt.Errorf("wintoast: lib object was nil") - } - - lib.Lock() - defer lib.Unlock() - - // Initialize all necessary string for the notification meta data - appNameUTF, _ := windows.UTF16PtrFromString(appName) - aumiUTF, _ := windows.UTF16PtrFromString(aumi) - linkUTF, _ := windows.UTF16PtrFromString(originalShortcutPath) - - // They are needed as unsafe pointers - appNameP := unsafe.Pointer(appNameUTF) - aumiP := unsafe.Pointer(aumiUTF) - linkP := unsafe.Pointer(linkUTF) - - // Initialize notifications - rc, _, err := lib.initialize.Call(uintptr(appNameP), uintptr(aumiP), uintptr(linkP)) - if rc != 0 { - return fmt.Errorf("wintoast: failed to initialize library rc = %d, %w", rc, err) - } - - // Check if if the initialization was successfully - rc, _, _ = lib.isInitialized.Call() - if rc == 1 { - lib.initialized.Set() - } else { - return fmt.Errorf("wintoast: initialized flag was not set: rc = %d", rc) - } - - return nil -} - -func (lib *WinToast) SetCallbacks(activated func(id int64, actionIndex int32), dismissed func(id int64, reason int32), failed func(id int64, reason int32)) error { - if lib == nil { - return fmt.Errorf("wintoast: lib object was nil") - } - - if lib.initialized.IsNotSet() { - return fmt.Errorf("winnotifiy: library not initialized") - } - - // Initialize notification activated callback - callback := windows.NewCallback(func(id int64, actionIndex int32) uint64 { - activated(id, actionIndex) - return 0 - }) - rc, _, err := lib.setActivatedCallback.Call(callback) - if rc != 1 { - return fmt.Errorf("winnotifiy: failed to initialize activated callback %w", err) - } - - // Initialize notification dismissed callback - callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 { - dismissed(id, actionIndex) - return 0 - }) - rc, _, err = lib.setDismissedCallback.Call(callback) - if rc != 1 { - return fmt.Errorf("winnotifiy: failed to initialize dismissed callback %w", err) - } - - // Initialize notification failed callback - callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 { - failed(id, actionIndex) - return 0 - }) - rc, _, err = lib.setFailedCallback.Call(callback) - if rc != 1 { - return fmt.Errorf("winnotifiy: failed to initialize failed callback %s", err) - } - - return nil -} - -// NewNotification starts a creation of new notification. NotificationBuilder.Delete should allays be called when done using the object or there will be memory leeks -func (lib *WinToast) NewNotification(title string, content string) (*NotificationBuilder, error) { - if lib == nil { - return nil, fmt.Errorf("wintoast: lib object was nil") - } - return newNotification(lib, title, content) -} - -// HideNotification hides notification -func (lib *WinToast) HideNotification(id int64) error { - if lib == nil { - return fmt.Errorf("wintoast: lib object was nil") - } - - lib.Lock() - defer lib.Unlock() - - rc, _, _ := lib.hideNotification.Call(uintptr(id)) - - if rc != 1 { - return fmt.Errorf("wintoast: failed to hide notification %d", id) - } - - return nil -} diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go index 0a96df6e..1cb9bafd 100644 --- a/cmds/observation-hub/main.go +++ b/cmds/observation-hub/main.go @@ -19,7 +19,6 @@ import ( "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/service/updates/helper" "github.com/safing/portmaster/spn" "github.com/safing/portmaster/spn/captain" "github.com/safing/portmaster/spn/conf" @@ -38,7 +37,6 @@ func main() { // Configure user agent and updates. updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH) - helper.IntelOnly() // Configure SPN mode. conf.EnableClient(true) diff --git a/cmds/portmaster-start/.gitignore b/cmds/portmaster-start/.gitignore deleted file mode 100644 index e3db7ed6..00000000 --- a/cmds/portmaster-start/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# binaries -portmaster-start -portmaster-start.exe - -# test dir -test diff --git a/cmds/portmaster-start/build b/cmds/portmaster-start/build deleted file mode 100755 index 38c552f5..00000000 --- a/cmds/portmaster-start/build +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -# get build data -if [[ "$BUILD_COMMIT" == "" ]]; then - BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null) -fi -if [[ "$BUILD_USER" == "" ]]; then - BUILD_USER=$(id -un) -fi -if [[ "$BUILD_HOST" == "" ]]; then - BUILD_HOST=$(hostname -f) -fi -if [[ "$BUILD_DATE" == "" ]]; then - BUILD_DATE=$(date +%d.%m.%Y) -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1) -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) -fi -BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g") - -# check -if [[ "$BUILD_COMMIT" == "" ]]; then - echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_USER" == "" ]]; then - echo "could not automatically determine BUILD_USER, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_HOST" == "" ]]; then - echo "could not automatically determine BUILD_HOST, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_DATE" == "" ]]; then - echo "could not automatically determine BUILD_DATE, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable." - exit 1 -fi - -# set build options -export CGO_ENABLED=0 - -# special handling for Windows -EXTRA_LD_FLAGS="" -if [[ $GOOS == "windows" ]]; then - # checks - if [[ $CC_FOR_windows_amd64 == "" ]]; then - echo "ENV variable CC_FOR_windows_amd64 (c compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64" - exit 1 - fi - if [[ $CXX_FOR_windows_amd64 == "" ]]; then - echo "ENV variable CXX_FOR_windows_amd64 (c++ compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64" - exit 1 - fi - # compilers - export CC=$CC_FOR_windows_amd64 - export CXX=$CXX_FOR_windows_amd64 - # custom - export CGO_ENABLED=1 - EXTRA_LD_FLAGS='-H windowsgui' # Hide console window by default (but we attach to parent console if available) - # generate resource.syso for windows metadata / icon - go generate -fi - -echo "Please notice, that this build script includes metadata into the build." -echo "This information is useful for debugging and license compliance." -echo "Run the compiled binary with the -version flag to see the information included." - -# build -BUILD_PATH="github.com/safing/portmaster/base/info" -go build -ldflags "$EXTRA_LD_FLAGS -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@" diff --git a/cmds/portmaster-start/console_default.go b/cmds/portmaster-start/console_default.go deleted file mode 100644 index f11a9fae..00000000 --- a/cmds/portmaster-start/console_default.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows - -package main - -import "os/exec" - -func attachToParentConsole() (attached bool, err error) { - return true, nil -} - -func hideWindow(cmd *exec.Cmd) {} diff --git a/cmds/portmaster-start/console_windows.go b/cmds/portmaster-start/console_windows.go deleted file mode 100644 index 1148d90e..00000000 --- a/cmds/portmaster-start/console_windows.go +++ /dev/null @@ -1,150 +0,0 @@ -package main - -// Parts of this file are FORKED -// from https://github.com/apenwarr/fixconsole/blob/35b2e7d921eb80a71a5f04f166ff0a1405bddf79/fixconsole_windows.go -// on 16.07.2019 -// with Apache-2.0 license -// authored by https://github.com/apenwarr - -// docs/sources: -// Stackoverflow Question: https://stackoverflow.com/questions/23743217/printing-output-to-a-command-window-when-golang-application-is-compiled-with-ld -// MS AttachConsole: https://docs.microsoft.com/en-us/windows/console/attachconsole - -import ( - "log" - "os" - "os/exec" - "syscall" - - "golang.org/x/sys/windows" -) - -const ( - windowsAttachParentProcess = ^uintptr(0) // (DWORD)-1 -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procAttachConsole = kernel32.NewProc("AttachConsole") -) - -// Windows console output is a mess. -// -// If you compile as "-H windows", then if you launch your program without -// a console, Windows forcibly creates one to use as your stdin/stdout, which -// is silly for a GUI app, so we can't do that. -// -// If you compile as "-H windowsgui", then it doesn't create a console for -// your app... but also doesn't provide a working stdin/stdout/stderr even if -// you *did* launch from the console. However, you can use AttachConsole() -// to get a handle to your parent process's console, if any, and then -// os.NewFile() to turn that handle into a fd usable as stdout/stderr. -// -// However, then you have the problem that if you redirect stdout or stderr -// from the shell, you end up ignoring the redirection by forcing it to the -// console. -// -// To fix *that*, we have to detect whether there was a pre-existing stdout -// or not. We can check GetStdHandle(), which returns 0 for "should be -// console" and nonzero for "already pointing at a file." -// -// Be careful though! As soon as you run AttachConsole(), it resets *all* -// the GetStdHandle() handles to point them at the console instead, thus -// throwing away the original file redirects. So we have to GetStdHandle() -// *before* AttachConsole(). -// -// For some reason, powershell redirections provide a valid file handle, but -// writing to that handle doesn't write to the file. I haven't found a way -// to work around that. (Windows 10.0.17763.379) -// -// Net result is as follows. -// Before: -// SHELL NON-REDIRECTED REDIRECTED -// explorer.exe no console n/a -// cmd.exe broken works -// powershell broken broken -// WSL bash broken works -// After -// SHELL NON-REDIRECTED REDIRECTED -// explorer.exe no console n/a -// cmd.exe works works -// powershell works broken -// WSL bash works works -// -// We don't seem to make anything worse, at least. -func attachToParentConsole() (attached bool, err error) { - // get std handles before we attempt to attach to parent console - stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) - stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) - stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) - - // attempt to attach to parent console - err = procAttachConsole.Find() - if err != nil { - return false, err - } - r1, _, _ := procAttachConsole.Call(windowsAttachParentProcess) - if r1 == 0 { - // possible errors: - // ERROR_ACCESS_DENIED: already attached to console - // ERROR_INVALID_HANDLE: process does not have console - // ERROR_INVALID_PARAMETER: process does not exist - return false, nil - } - - // get std handles after we attached to console - var invalid syscall.Handle - con := invalid - - if stdin == invalid { - stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) - } - if stdout == invalid { - stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) - con = stdout - } - if stderr == invalid { - stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) - con = stderr - } - - // correct output mode - if con != invalid { - // Make sure the console is configured to convert - // \n to \r\n, like Go programs expect. - h := windows.Handle(con) - var st uint32 - err := windows.GetConsoleMode(h, &st) - if err != nil { - log.Printf("failed to get console mode: %s\n", err) - } else { - err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN) - if err != nil { - log.Printf("failed to set console mode: %s\n", err) - } - } - } - - // fix std handles to correct values (ie. redirects) - if stdin != invalid { - os.Stdin = os.NewFile(uintptr(stdin), "stdin") - log.Printf("fixed os.Stdin after attaching to parent console\n") - } - if stdout != invalid { - os.Stdout = os.NewFile(uintptr(stdout), "stdout") - log.Printf("fixed os.Stdout after attaching to parent console\n") - } - if stderr != invalid { - os.Stderr = os.NewFile(uintptr(stderr), "stderr") - log.Printf("fixed os.Stderr after attaching to parent console\n") - } - - log.Println("attached to parent console") - return true, nil -} - -func hideWindow(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - } -} diff --git a/cmds/portmaster-start/dirs.go b/cmds/portmaster-start/dirs.go deleted file mode 100644 index e327963f..00000000 --- a/cmds/portmaster-start/dirs.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(cleanStructureCmd) -} - -var cleanStructureCmd = &cobra.Command{ - Use: "clean-structure", - Short: "Create and clean the required directory structure", - RunE: func(cmd *cobra.Command, args []string) error { - if err := ensureLoggingDir(); err != nil { - return err - } - return cleanAndEnsureExecDir() - }, -} - -func cleanAndEnsureExecDir() error { - execDir := dataRoot.ChildDir("exec", 0o777) - - // Clean up and remove exec dir. - err := os.RemoveAll(execDir.Path) - if err != nil { - log.Printf("WARNING: failed to fully remove exec dir (%q) for cleaning: %s", execDir.Path, err) - } - - // Re-create exec dir. - err = execDir.Ensure() - if err != nil { - return fmt.Errorf("failed to initialize exec dir (%q): %w", execDir.Path, err) - } - - return nil -} diff --git a/cmds/portmaster-start/install_windows.go b/cmds/portmaster-start/install_windows.go deleted file mode 100644 index 5f1d5bb2..00000000 --- a/cmds/portmaster-start/install_windows.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -// Based on the official Go examples from -// https://github.com/golang/sys/blob/master/windows/svc/example -// by The Go Authors. -// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - "time" - - "github.com/spf13/cobra" - "golang.org/x/sys/windows" - "golang.org/x/sys/windows/svc" - "golang.org/x/sys/windows/svc/mgr" -) - -func init() { - rootCmd.AddCommand(installCmd) - installCmd.AddCommand(installService) - - rootCmd.AddCommand(uninstallCmd) - uninstallCmd.AddCommand(uninstallService) -} - -var installCmd = &cobra.Command{ - Use: "install", - Short: "Install system integrations", -} - -var uninstallCmd = &cobra.Command{ - Use: "uninstall", - Short: "Uninstall system integrations", -} - -var installService = &cobra.Command{ - Use: "core-service", - Short: "Install Portmaster Core Windows Service", - RunE: installWindowsService, -} - -var uninstallService = &cobra.Command{ - Use: "core-service", - Short: "Uninstall Portmaster Core Windows Service", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // non-nil dummy to override db flag requirement - return nil - }, - RunE: uninstallWindowsService, -} - -func getAbsBinaryPath() (string, error) { - p, err := filepath.Abs(os.Args[0]) - if err != nil { - return "", err - } - - return p, nil -} - -func getServiceExecCommand(exePath string, escape bool) []string { - return []string{ - maybeEscape(exePath, escape), - "core-service", - "--data", - maybeEscape(dataRoot.Path, escape), - "--input-signals", - } -} - -func maybeEscape(s string, escape bool) string { - if escape { - return windows.EscapeArg(s) - } - return s -} - -func getServiceConfig(exePath string) mgr.Config { - return mgr.Config{ - ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, - StartType: mgr.StartAutomatic, - ErrorControl: mgr.ErrorNormal, - BinaryPathName: strings.Join(getServiceExecCommand(exePath, true), " "), - DisplayName: "Portmaster Core", - Description: "Portmaster Application Firewall - Core Service", - } -} - -func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uint32) { - return []mgr.RecoveryAction{ - { - Type: mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand - Delay: 1 * time.Minute, // the time to wait before performing the specified action - }, - }, 86400 -} - -func installWindowsService(cmd *cobra.Command, args []string) error { - // get exe path - exePath, err := getAbsBinaryPath() - if err != nil { - return fmt.Errorf("failed to get exe path: %s", err) - } - - // connect to Windows service manager - m, err := mgr.Connect() - if err != nil { - return fmt.Errorf("failed to connect to service manager: %s", err) - } - defer m.Disconnect() //nolint:errcheck // TODO - - // open service - created := false - s, err := m.OpenService(serviceName) - if err != nil { - // create service - cmd := getServiceExecCommand(exePath, false) - s, err = m.CreateService(serviceName, cmd[0], getServiceConfig(exePath), cmd[1:]...) - if err != nil { - return fmt.Errorf("failed to create service: %s", err) - } - defer s.Close() - created = true - } else { - // update service - err = s.UpdateConfig(getServiceConfig(exePath)) - if err != nil { - return fmt.Errorf("failed to update service: %s", err) - } - defer s.Close() - } - - // update recovery actions - err = s.SetRecoveryActions(getRecoveryActions()) - if err != nil { - return fmt.Errorf("failed to update recovery actions: %s", err) - } - - if created { - log.Printf("created service %s\n", serviceName) - } else { - log.Printf("updated service %s\n", serviceName) - } - - return nil -} - -func uninstallWindowsService(cmd *cobra.Command, args []string) error { - // connect to Windows service manager - m, err := mgr.Connect() - if err != nil { - return err - } - defer m.Disconnect() //nolint:errcheck // we don't care if we failed to disconnect from the service manager, we're quitting anyway. - - // open service - s, err := m.OpenService(serviceName) - if err != nil { - return fmt.Errorf("service %s is not installed", serviceName) - } - defer s.Close() - - _, err = s.Control(svc.Stop) - if err != nil { - log.Printf("failed to stop service: %s\n", err) - } - - // delete service - err = s.Delete() - if err != nil { - return fmt.Errorf("failed to delete service: %s", err) - } - - log.Printf("uninstalled service %s\n", serviceName) - return nil -} diff --git a/cmds/portmaster-start/lock.go b/cmds/portmaster-start/lock.go deleted file mode 100644 index 0526084c..00000000 --- a/cmds/portmaster-start/lock.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/fs" - "log" - "os" - "os/user" - "path/filepath" - "strconv" - "strings" - - processInfo "github.com/shirou/gopsutil/process" -) - -func checkAndCreateInstanceLock(path, name string, perUser bool) (pid int32, err error) { - lockFilePath := getLockFilePath(path, name, perUser) - - // read current pid file - data, err := os.ReadFile(lockFilePath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - // create new lock - return 0, createInstanceLock(lockFilePath) - } - return 0, err - } - - // file exists! - parsedPid, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) - if err != nil { - log.Printf("failed to parse existing lock pid file (ignoring): %s\n", err) - return 0, createInstanceLock(lockFilePath) - } - - // Check if process exists. - p, err := processInfo.NewProcess(int32(parsedPid)) - switch { - case err == nil: - // Process exists, continue. - case errors.Is(err, processInfo.ErrorProcessNotRunning): - // A process with the locked PID does not exist. - // This is expected, so we can continue normally. - return 0, createInstanceLock(lockFilePath) - default: - // There was an internal error getting the process. - return 0, err - } - - // Get the process paths and evaluate and clean them. - executingBinaryPath, err := p.Exe() - if err != nil { - return 0, fmt.Errorf("failed to get path of existing process: %w", err) - } - cleanedExecutingBinaryPath, err := filepath.EvalSymlinks(executingBinaryPath) - if err != nil { - return 0, fmt.Errorf("failed to evaluate path of existing process: %w", err) - } - - // Check if the binary is portmaster-start with high probability. - if !strings.Contains(filepath.Base(cleanedExecutingBinaryPath), "portmaster-start") { - // The process with the locked PID belongs to another binary. - // As the Portmaster usually starts very early, it will have a low PID, - // which could be assigned to another process on next boot. - return 0, createInstanceLock(lockFilePath) - } - - // Return PID of already running instance. - return p.Pid, nil -} - -func createInstanceLock(lockFilePath string) error { - // check data root dir - err := dataRoot.Ensure() - if err != nil { - log.Printf("failed to check data root dir: %s\n", err) - } - - // create lock file - // TODO: Investigate required permissions. - err = os.WriteFile(lockFilePath, []byte(strconv.Itoa(os.Getpid())), 0o0666) //nolint:gosec - if err != nil { - return err - } - - return nil -} - -func deleteInstanceLock(path, name string, perUser bool) error { - return os.Remove(getLockFilePath(path, name, perUser)) -} - -func getLockFilePath(path, name string, perUser bool) string { - if !perUser { - return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-lock.pid", name)) - } - - // Get user ID for per-user lock file. - var userID string - usr, err := user.Current() - if err != nil { - log.Printf("failed to get current user: %s\n", err) - userID = "no-user" - } else { - userID = usr.Uid - } - return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-%s-lock.pid", name, userID)) -} diff --git a/cmds/portmaster-start/logs.go b/cmds/portmaster-start/logs.go deleted file mode 100644 index f2f514b9..00000000 --- a/cmds/portmaster-start/logs.go +++ /dev/null @@ -1,127 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - "time" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/database/record" - "github.com/safing/portmaster/base/info" - "github.com/safing/structures/container" - "github.com/safing/structures/dsd" -) - -func initializeLogFile(logFilePath string, identifier string, version string) *os.File { - logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE, 0o0440) //nolint:gosec // As desired. - if err != nil { - log.Printf("failed to create log file %s: %s\n", logFilePath, err) - return nil - } - - // create header, so that the portmaster can view log files as a database - meta := record.Meta{} - meta.Update() - meta.SetAbsoluteExpiry(time.Now().Add(720 * time.Hour).Unix()) // one month - - // manually marshal - // version - c := container.New([]byte{1}) - // meta - metaSection, err := dsd.Dump(meta, dsd.JSON) - if err != nil { - log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err) - finalizeLogFile(logFile) - return nil - } - c.AppendAsBlock(metaSection) - // log file data type (string) and newline for better manual viewing - c.Append([]byte("S\n")) - c.Append([]byte(fmt.Sprintf("executing %s version %s on %s %s\n", identifier, version, runtime.GOOS, runtime.GOARCH))) - - _, err = logFile.Write(c.CompileData()) - if err != nil { - log.Printf("failed to write header for log file %s: %s\n", logFilePath, err) - finalizeLogFile(logFile) - return nil - } - - return logFile -} - -func finalizeLogFile(logFile *os.File) { - logFilePath := logFile.Name() - - err := logFile.Close() - if err != nil { - log.Printf("failed to close log file %s: %s\n", logFilePath, err) - } - - // check file size - stat, err := os.Stat(logFilePath) - if err != nil { - return - } - - // delete if file is smaller than - if stat.Size() >= 200 { // header + info is about 150 bytes - return - } - - if err := os.Remove(logFilePath); err != nil { - log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err) - } -} - -func getLogFile(options *Options, version, ext string) *os.File { - // check logging dir - logFileBasePath := filepath.Join(logsRoot.Path, options.ShortIdentifier) - err := logsRoot.EnsureAbsPath(logFileBasePath) - if err != nil { - log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err) - } - - // open log file - logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s%s", time.Now().UTC().Format("2006-01-02-15-04-05"), ext)) - return initializeLogFile(logFilePath, options.Identifier, version) -} - -func getPmStartLogFile(ext string) *os.File { - return getLogFile(&Options{ - ShortIdentifier: "start", - Identifier: "start/portmaster-start", - }, info.Version(), ext) -} - -//nolint:unused // false positive on linux, currently used by windows only. TODO: move to a _windows file. -func logControlError(cErr error) { - // check if error present - if cErr == nil { - return - } - - errorFile := getPmStartLogFile(".error.log") - if errorFile == nil { - return - } - defer func() { - _ = errorFile.Close() - }() - - fmt.Fprintln(errorFile, cErr.Error()) -} - -//nolint:deadcode,unused // false positive on linux, currently used by windows only. TODO: move to a _windows file. -func runAndLogControlError(wrappedFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - err := wrappedFunc(cmd, args) - if err != nil { - logControlError(err) - } - return err - } -} diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go deleted file mode 100644 index f764dfbf..00000000 --- a/cmds/portmaster-start/main.go +++ /dev/null @@ -1,257 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - "net/url" - "os" - "os/signal" - "path/filepath" - "runtime" - "strings" - "syscall" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/info" - portlog "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - dataDir string - maxRetries int - dataRoot *utils.DirStructure - logsRoot *utils.DirStructure - forceOldUI bool - - updateURLFlag string - userAgentFlag string - - // Create registry. - registry = &updater.ResourceRegistry{ - Name: "updates", - UpdateURLs: []string{ - "https://updates.safing.io", - }, - UserAgent: fmt.Sprintf("Portmaster Start (%s %s)", runtime.GOOS, runtime.GOARCH), - Verification: helper.VerificationConfig, - DevMode: false, - Online: true, // is disabled later based on command - } - - rootCmd = &cobra.Command{ - Use: "portmaster-start", - Short: "Start Portmaster components", - PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { - mustLoadIndex := indexRequired(cmd) - if err := configureRegistry(mustLoadIndex); err != nil { - return err - } - - if err := ensureLoggingDir(); err != nil { - return err - } - - return nil - }, - SilenceUsage: true, - } -) - -func init() { - // Let cobra ignore if we are running as "GUI" or not - cobra.MousetrapHelpText = "" - - flags := rootCmd.PersistentFlags() - { - flags.StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.") - flags.StringVar(&updateURLFlag, "update-server", "", "Set an alternative update server (full URL)") - flags.StringVar(&userAgentFlag, "update-agent", "", "Set an alternative user agent for requests to the update server") - flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component") - flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdin.") - flags.BoolVar(&forceOldUI, "old-ui", false, "Use the old ui. (Beta)") - _ = rootCmd.MarkPersistentFlagDirname("data") - _ = flags.MarkHidden("input-signals") - } -} - -func main() { - cobra.OnInitialize(initCobra) - - // set meta info - info.Set("Portmaster Start", "", "GPLv3") - - // catch interrupt for clean shutdown - signalCh := make(chan os.Signal, 2) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // start root command - go func() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } - os.Exit(0) - }() - - // wait for signals - for sig := range signalCh { - if childIsRunning.IsSet() { - log.Printf("got %s signal (ignoring), waiting for child to exit...\n", sig) - continue - } - - log.Printf("got %s signal, exiting... (not executing anything)\n", sig) - os.Exit(0) - } -} - -func initCobra() { - // check if we are running in a console (try to attach to parent console if available) - var err error - runningInConsole, err = attachToParentConsole() - if err != nil { - log.Fatalf("failed to attach to parent console: %s\n", err) - } - - // check if meta info is ok - err = info.CheckVersion() - if err != nil { - log.Fatalf("compile error: please compile using the provided build script") - } - - // set up logging - log.SetFlags(log.Ldate | log.Ltime | log.LUTC) - log.SetPrefix("[pmstart] ") - log.SetOutput(os.Stdout) - - // not using portbase logger - portlog.SetLogLevel(portlog.CriticalLevel) -} - -func configureRegistry(mustLoadIndex bool) error { - // Check if update server URL supplied via flag is a valid URL. - if updateURLFlag != "" { - u, err := url.Parse(updateURLFlag) - if err != nil { - return fmt.Errorf("supplied update server URL is invalid: %w", err) - } - if u.Scheme != "https" { - return errors.New("supplied update server URL must use HTTPS") - } - } - - // Override values from flags. - if userAgentFlag != "" { - registry.UserAgent = userAgentFlag - } - if updateURLFlag != "" { - registry.UpdateURLs = []string{updateURLFlag} - } - - // If dataDir is not set, check the environment variable. - if dataDir == "" { - dataDir = os.Getenv("PORTMASTER_DATA") - } - - // If it's still empty, try to auto-detect it. - if dataDir == "" { - dataDir = detectInstallationDir() - } - - // Finally, if it's still empty, the user must provide it. - if dataDir == "" { - return errors.New("please set the data directory using --data=/path/to/data/dir") - } - - // Remove left over quotes. - dataDir = strings.Trim(dataDir, `\"`) - // Initialize data root. - err := dataroot.Initialize(dataDir, 0o0755) - if err != nil { - return fmt.Errorf("failed to initialize data root: %w", err) - } - dataRoot = dataroot.Root() - - // Initialize registry. - err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755)) - if err != nil { - return err - } - - return updateRegistryIndex(mustLoadIndex) -} - -func ensureLoggingDir() error { - // set up logs root - logsRoot = dataRoot.ChildDir("logs", 0o0777) - err := logsRoot.Ensure() - if err != nil { - return fmt.Errorf("failed to initialize logs root (%q): %w", logsRoot.Path, err) - } - - // warn about CTRL-C on windows - if runningInConsole && onWindows { - log.Println("WARNING: portmaster-start is marked as a GUI application in order to get rid of the console window.") - log.Println("WARNING: CTRL-C will immediately kill without clean shutdown.") - } - return nil -} - -func updateRegistryIndex(mustLoadIndex bool) error { - // Set indexes based on the release channel. - warning := helper.SetIndexes(registry, "", false, false, false) - if warning != nil { - log.Printf("WARNING: %s\n", warning) - } - - // Load indexes from disk or network, if needed and desired. - err := registry.LoadIndexes(context.Background()) - if err != nil { - log.Printf("WARNING: error loading indexes: %s\n", err) - if mustLoadIndex { - return err - } - } - - // Load versions from disk to know which others we have and which are available. - err = registry.ScanStorage("") - if err != nil { - log.Printf("WARNING: error during storage scan: %s\n", err) - } - - registry.SelectVersions() - return nil -} - -func detectInstallationDir() string { - exePath, err := filepath.Abs(os.Args[0]) - if err != nil { - return "" - } - - parent := filepath.Dir(exePath) - stableJSONFile := filepath.Join(parent, "updates", "stable.json") - stat, err := os.Stat(stableJSONFile) - if err != nil { - return "" - } - - if stat.IsDir() { - return "" - } - - return parent -} diff --git a/cmds/portmaster-start/pack b/cmds/portmaster-start/pack deleted file mode 100755 index 1cf6ca2a..00000000 --- a/cmds/portmaster-start/pack +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash - -baseDir="$( cd "$(dirname "$0")" && pwd )" -cd "$baseDir" - -COL_OFF="\033[0m" -COL_BOLD="\033[01;01m" -COL_RED="\033[31m" -COL_GREEN="\033[32m" -COL_YELLOW="\033[33m" - -destDirPart1="../../dist" -destDirPart2="start" - -function prep { - # output - output="portmaster-start" - # get version - version=$(grep "info.Set" main.go | cut -d'"' -f4) - # build versioned file name - filename="portmaster-start_v${version//./-}" - # platform - platform="${GOOS}_${GOARCH}" - if [[ $GOOS == "windows" ]]; then - filename="${filename}.exe" - output="${output}.exe" - fi - # build destination path - destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename -} - -function check { - prep - - # check if file exists - if [[ -f $destPath ]]; then - echo "[start] $platform $version already built" - else - echo -e "${COL_BOLD}[start] $platform v$version${COL_OFF}" - fi -} - -function build { - prep - - # check if file exists - if [[ -f $destPath ]]; then - echo "[start] $platform already built in v$version, skipping..." - return - fi - - # build - ./build - if [[ $? -ne 0 ]]; then - echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}" - exit 1 - fi - mkdir -p $(dirname $destPath) - cp $output $destPath - echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}" -} - -function reset { - prep - - # delete if file exists - if [[ -f $destPath ]]; then - rm $destPath - echo "[start] $platform v$version deleted." - fi -} - -function check_all { - GOOS=linux GOARCH=amd64 check - GOOS=windows GOARCH=amd64 check - GOOS=darwin GOARCH=amd64 check - GOOS=linux GOARCH=arm64 check - GOOS=windows GOARCH=arm64 check - GOOS=darwin GOARCH=arm64 check -} - -function build_all { - GOOS=linux GOARCH=amd64 build - GOOS=windows GOARCH=amd64 build - GOOS=darwin GOARCH=amd64 build - GOOS=linux GOARCH=arm64 build - GOOS=windows GOARCH=arm64 build - GOOS=darwin GOARCH=arm64 build -} - -function reset_all { - GOOS=linux GOARCH=amd64 reset - GOOS=windows GOARCH=amd64 reset - GOOS=darwin GOARCH=amd64 reset - GOOS=linux GOARCH=arm64 reset - GOOS=windows GOARCH=arm64 reset - GOOS=darwin GOARCH=arm64 reset -} - -case $1 in - "check" ) - check_all - ;; - "build" ) - build_all - ;; - "reset" ) - reset_all - ;; - * ) - echo "" - echo "build list:" - echo "" - check_all - echo "" - read -p "press [Enter] to start building" x - echo "" - build_all - echo "" - echo "finished building." - echo "" - ;; -esac diff --git a/cmds/portmaster-start/recover_linux.go b/cmds/portmaster-start/recover_linux.go deleted file mode 100644 index 96719bd8..00000000 --- a/cmds/portmaster-start/recover_linux.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/hashicorp/go-multierror" - "github.com/spf13/cobra" - - "github.com/safing/portmaster/service/firewall/interception" -) - -var recoverIPTablesCmd = &cobra.Command{ - Use: "recover-iptables", - Short: "Removes obsolete IP tables rules in case of an unclean shutdown", - RunE: func(*cobra.Command, []string) error { - // interception.DeactiveNfqueueFirewall uses coreos/go-iptables - // which shells out to the /sbin/iptables binary. As a result, - // we don't get the errno of the actual error and need to parse the - // output instead. Make sure it's always english by setting LC_ALL=C - currentLocale := os.Getenv("LC_ALL") - _ = os.Setenv("LC_ALL", "C") - defer func() { - _ = os.Setenv("LC_ALL", currentLocale) - }() - - err := interception.DeactivateNfqueueFirewall() - if err == nil { - return nil - } - - // we don't want to show ErrNotExists to the user - // as that only means portmaster did the cleanup itself. - var mr *multierror.Error - if !errors.As(err, &mr) { - return err - } - - var filteredErrors *multierror.Error - for _, err := range mr.Errors { - // if we have a permission denied error, all errors will be the same - if strings.Contains(err.Error(), "Permission denied") { - return fmt.Errorf("failed to cleanup iptables: %w", os.ErrPermission) - } - - if !strings.Contains(err.Error(), "No such file or directory") { - filteredErrors = multierror.Append(filteredErrors, err) - } - } - - if filteredErrors != nil { - filteredErrors.ErrorFormat = formatNfqErrors - return filteredErrors.ErrorOrNil() - } - - return nil - }, - SilenceUsage: true, -} - -func init() { - rootCmd.AddCommand(recoverIPTablesCmd) -} - -func formatNfqErrors(es []error) string { - if len(es) == 1 { - return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) - } - - points := make([]string, len(es)) - for i, err := range es { - // only display the very first line of each error - first := strings.Split(err.Error(), "\n")[0] - points[i] = fmt.Sprintf("* %s", first) - } - - return fmt.Sprintf( - "%d errors occurred:\n\t%s\n\n", - len(es), strings.Join(points, "\n\t")) -} diff --git a/cmds/portmaster-start/run.go b/cmds/portmaster-start/run.go deleted file mode 100644 index f807fd69..00000000 --- a/cmds/portmaster-start/run.go +++ /dev/null @@ -1,486 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - "log" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/spf13/cobra" - "github.com/tevino/abool" - - "github.com/safing/portmaster/service/updates/helper" -) - -const ( - // RestartExitCode is the exit code that any service started by portmaster-start - // can return in order to trigger a restart after a clean shutdown. - RestartExitCode = 23 - - // ControlledFailureExitCode is the exit code that any service started by - // portmaster-start can return in order to signify a controlled failure. - // This disables retrying and exits with an error code. - ControlledFailureExitCode = 24 - - // StartOldUIExitCode is an exit code that is returned by the UI when there. This is manfully triaged by the user, if the new UI does not work for them. - StartOldUIExitCode = 77 - MissingDependencyExitCode = 0xc0000135 // Windows STATUS_DLL_NOT_FOUND - - exeSuffix = ".exe" - zipSuffix = ".zip" -) - -var ( - runningInConsole bool - onWindows = runtime.GOOS == "windows" - stdinSignals bool - childIsRunning = abool.NewBool(false) - - fallBackToOldUI bool = false -) - -// Options for starting component. -type Options struct { - Name string - Identifier string // component identifier - ShortIdentifier string // populated automatically - LockPathPrefix string - LockPerUser bool - PIDFile bool - SuppressArgs bool // do not use any args - AllowDownload bool // allow download of component if it is not yet available - AllowHidingWindow bool // allow hiding the window of the subprocess - NoOutput bool // do not use stdout/err if logging to file is available (did not fail to open log file) - RestartOnFail bool // Try restarting automatically, if the started component fails. -} - -// This is a temp value that will be used to test the new UI in beta. -var app2Options = Options{ - Name: "Portmaster App2", - Identifier: "app2/portmaster-app", - AllowDownload: false, - AllowHidingWindow: false, - RestartOnFail: true, -} - -func init() { - // Make sure the new UI has a proper extension. - if onWindows { - app2Options.Identifier += ".zip" - } - - registerComponent([]Options{ - { - Name: "Portmaster Core", - Identifier: "core/portmaster-core", - AllowDownload: true, - AllowHidingWindow: true, - PIDFile: true, - RestartOnFail: true, - }, - { - Name: "Portmaster App", - Identifier: "app/portmaster-app.zip", - AllowDownload: false, - AllowHidingWindow: false, - RestartOnFail: true, - }, - { - Name: "Portmaster Notifier", - Identifier: "notifier/portmaster-notifier", - LockPerUser: true, - AllowDownload: false, - AllowHidingWindow: true, - PIDFile: true, - LockPathPrefix: "exec", - }, - { - Name: "Safing Privacy Network", - Identifier: "hub/spn-hub", - AllowDownload: true, - AllowHidingWindow: true, - PIDFile: true, - RestartOnFail: true, - }, - app2Options, - }) -} - -func registerComponent(opts []Options) { - for idx := range opts { - opt := &opts[idx] // we need a copy - if opt.ShortIdentifier == "" { - opt.ShortIdentifier = path.Dir(opt.Identifier) - } - - rootCmd.AddCommand( - &cobra.Command{ - Use: opt.ShortIdentifier, - Short: "Run the " + opt.Name, - RunE: func(cmd *cobra.Command, args []string) error { - err := run(opt, args) - initiateShutdown(err) - return err - }, - }, - ) - - showCmd.AddCommand( - &cobra.Command{ - Use: opt.ShortIdentifier, - Short: "Show command to execute the " + opt.Name, - RunE: func(cmd *cobra.Command, args []string) error { - return show(opt, args) - }, - }, - ) - } -} - -func getExecArgs(opts *Options, cmdArgs []string) []string { - if opts.SuppressArgs { - return nil - } - - args := []string{"--data", dataDir} - if stdinSignals { - args = append(args, "--input-signals") - } - - if runtime.GOOS == "linux" && opts.ShortIdentifier == "app" { - // see https://www.freedesktop.org/software/systemd/man/pam_systemd.html#type= - if xdgSessionType := os.Getenv("XDG_SESSION_TYPE"); xdgSessionType == "wayland" { - // we're running the Portmaster UI App under Wayland so make sure we add some arguments - // required by Electron. - args = append(args, - []string{ - "--enable-features=UseOzonePlatform,WaylandWindowDecorations", - "--ozone-platform=wayland", - }..., - ) - } - } - - args = append(args, cmdArgs...) - return args -} - -func run(opts *Options, cmdArgs []string) (err error) { - // set download option - registry.Online = opts.AllowDownload - - if isShuttingDown() { - return nil - } - - // check for duplicate instances - if opts.PIDFile { - pid, err := checkAndCreateInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser) - if err != nil { - return fmt.Errorf("failed to exec lock: %w", err) - } - if pid != 0 { - return fmt.Errorf("another instance of %s is already running: PID %d", opts.Name, pid) - } - defer func() { - err := deleteInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser) - if err != nil { - log.Printf("failed to delete instance lock: %s\n", err) - } - }() - } - - // notify service after some time - go func() { - // assume that after 3 seconds service has finished starting - time.Sleep(3 * time.Second) - startupComplete <- struct{}{} - }() - - // adapt identifier - if onWindows && !strings.HasSuffix(opts.Identifier, zipSuffix) { - opts.Identifier += exeSuffix - } - - // setup logging - // init log file - logFile := getPmStartLogFile(".log") - if logFile != nil { - // don't close logFile, will be closed by system - if opts.NoOutput { - log.Println("disabling log output to stdout... bye!") - log.SetOutput(logFile) - } else { - log.SetOutput(io.MultiWriter(os.Stdout, logFile)) - } - } - - return runAndRestart(opts, cmdArgs) -} - -func runAndRestart(opts *Options, args []string) error { - tries := 0 - for { - tryAgain, err := execute(opts, args) - if err != nil { - log.Printf("%s failed with: %s\n", opts.Identifier, err) - tries++ - if tries >= maxRetries { - log.Printf("encountered %d consecutive errors, giving up ...", tries) - return err - } - } else { - tries = 0 - log.Printf("%s exited without error", opts.Identifier) - } - - if !opts.RestartOnFail || !tryAgain { - return err - } - - // if a restart was requested `tries` is set to 0 so - // this becomes a no-op. - time.Sleep(time.Duration(2*tries) * time.Second) - - if tries >= 2 || err == nil { - // if we are constantly failing or a restart was requested - // try to update the resources. - log.Printf("updating registry index") - _ = updateRegistryIndex(false) // will always return nil - } - } -} - -func fixExecPerm(path string) error { - if onWindows { - return nil - } - - info, err := os.Stat(path) - if err != nil { - return fmt.Errorf("failed to stat %s: %w", path, err) - } - - if info.Mode() == 0o0755 { - return nil - } - - if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights. - return fmt.Errorf("failed to chmod %s: %w", path, err) - } - - return nil -} - -func copyLogs(opts *Options, consoleSink io.Writer, version, ext string, logSource io.Reader, notifier chan<- struct{}) { - defer func() { notifier <- struct{}{} }() - - sink := consoleSink - - fileSink := getLogFile(opts, version, ext) - if fileSink != nil { - defer finalizeLogFile(fileSink) - if opts.NoOutput { - sink = fileSink - } else { - sink = io.MultiWriter(consoleSink, fileSink) - } - } - - if bytes, err := io.Copy(sink, logSource); err != nil { - log.Printf("%s: writing logs failed after %d bytes: %s", fileSink.Name(), bytes, err) - } -} - -func persistOutputStreams(opts *Options, version string, cmd *exec.Cmd) (chan struct{}, error) { - var ( - done = make(chan struct{}) - copyNotifier = make(chan struct{}, 2) - ) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("failed to connect stdout: %w", err) - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, fmt.Errorf("failed to connect stderr: %w", err) - } - - go copyLogs(opts, os.Stdout, version, ".log", stdout, copyNotifier) - go copyLogs(opts, os.Stderr, version, ".error.log", stderr, copyNotifier) - - go func() { - <-copyNotifier - <-copyNotifier - close(copyNotifier) - close(done) - }() - - return done, nil -} - -func execute(opts *Options, args []string) (cont bool, err error) { - // Auto-upgrade to new UI if in beta and new UI is not disabled or failed. - if opts.ShortIdentifier == "app" && - registry.UsePreReleases && - !forceOldUI && - !fallBackToOldUI { - log.Println("auto-upgraded to new UI") - opts = &app2Options - } - - // Compile arguments and add additional arguments based on system configuration. - // Extra parameters can be specified using "-- --some-parameter". - args = getExecArgs(opts, args) - - file, err := registry.GetFile( - helper.PlatformIdentifier(opts.Identifier), - ) - if err != nil { - return true, fmt.Errorf("could not get component: %w", err) - } - binPath := file.Path() - - // Adapt path for packaged software. - if strings.HasSuffix(binPath, zipSuffix) { - // Remove suffix from binary path. - binPath = strings.TrimSuffix(binPath, zipSuffix) - // Add binary with the same name to access the unpacked binary. - binPath = filepath.Join(binPath, filepath.Base(binPath)) - - // Adapt binary path on Windows. - if onWindows { - binPath += exeSuffix - } - } - - // check permission - if err := fixExecPerm(binPath); err != nil { - return true, err - } - - log.Printf("starting %s %s\n", binPath, strings.Join(args, " ")) - - // create command - exc := exec.Command(binPath, args...) - - if !runningInConsole && opts.AllowHidingWindow { - // Windows only: - // only hide (all) windows of program if we are not running in console and windows may be hidden - hideWindow(exc) - } - - outputsWritten, err := persistOutputStreams(opts, file.Version(), exc) - if err != nil { - return true, err - } - - interrupt, err := getProcessSignalFunc(exc) - if err != nil { - return true, err - } - - err = exc.Start() - if err != nil { - return true, fmt.Errorf("failed to start %s: %w", opts.Identifier, err) - } - childIsRunning.Set() - - // wait for completion - finished := make(chan error, 1) - go func() { - defer close(finished) - - <-outputsWritten - // wait for process to return - finished <- exc.Wait() - // update status - childIsRunning.UnSet() - }() - - // state change listeners - select { - case <-shuttingDown: - if err := interrupt(); err != nil { - log.Printf("failed to signal %s to shutdown: %s\n", opts.Identifier, err) - err = exc.Process.Kill() - if err != nil { - return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err) - } - return false, fmt.Errorf("killed %s", opts.Identifier) - } - - // wait until shut down - select { - case <-finished: - case <-time.After(3 * time.Minute): // portmaster core prints stack if not able to shutdown in 3 minutes, give it one more ... - err = exc.Process.Kill() - if err != nil { - return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err) - } - return false, fmt.Errorf("killed %s", opts.Identifier) - } - return false, nil - - case err := <-finished: - return parseExitError(err) - } -} - -func getProcessSignalFunc(cmd *exec.Cmd) (func() error, error) { - if stdinSignals { - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, fmt.Errorf("failed to connect stdin: %w", err) - } - - return func() error { - _, err := fmt.Fprintln(stdin, "SIGINT") - return err - }, nil - } - - return func() error { - return cmd.Process.Signal(os.Interrupt) - }, nil -} - -func parseExitError(err error) (restart bool, errWithCtx error) { - if err == nil { - // clean and coordinated exit - return false, nil - } - - var exErr *exec.ExitError - if errors.As(err, &exErr) { - switch exErr.ProcessState.ExitCode() { - case 0: - return false, fmt.Errorf("clean exit with error: %w", err) - case 1: - return true, fmt.Errorf("error during execution: %w", err) - case RestartExitCode: - return true, nil - case ControlledFailureExitCode: - return false, errors.New("controlled failure, check logs") - case StartOldUIExitCode: - fallBackToOldUI = true - return true, errors.New("user requested old UI") - case MissingDependencyExitCode: - fallBackToOldUI = true - return true, errors.New("new UI failed with missing dependency") - default: - return true, fmt.Errorf("unknown exit code %w", exErr) - } - } - - return true, fmt.Errorf("unexpected error type: %w", err) -} diff --git a/cmds/portmaster-start/service_windows.go b/cmds/portmaster-start/service_windows.go deleted file mode 100644 index bd47c5b2..00000000 --- a/cmds/portmaster-start/service_windows.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -// Based on the official Go examples from -// https://github.com/golang/sys/blob/master/windows/svc/example -// by The Go Authors. -// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/spf13/cobra" - "golang.org/x/sys/windows/svc" - "golang.org/x/sys/windows/svc/debug" -) - -var ( - runCoreService = &cobra.Command{ - Use: "core-service", - Short: "Run the Portmaster Core as a Windows Service", - RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error { - return runService(cmd, &Options{ - Name: "Portmaster Core Service", - Identifier: "core/portmaster-core", - ShortIdentifier: "core", - AllowDownload: true, - AllowHidingWindow: false, - NoOutput: true, - RestartOnFail: true, - }, args) - }), - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, - } - - // wait groups - runWg sync.WaitGroup - finishWg sync.WaitGroup -) - -func init() { - rootCmd.AddCommand(runCoreService) -} - -const serviceName = "PortmasterCore" - -type windowsService struct{} - -func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { - const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown - changes <- svc.Status{State: svc.StartPending} - -service: - for { - select { - case <-startupComplete: - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - case <-shuttingDown: - changes <- svc.Status{State: svc.StopPending} - break service - case c := <-changeRequests: - switch c.Cmd { - case svc.Interrogate: - changes <- c.CurrentStatus - case svc.Stop, svc.Shutdown: - initiateShutdown(nil) - default: - log.Printf("unexpected control request: #%d\n", c) - } - } - } - - // define return values - if getShutdownError() != nil { - ssec = true // this error is specific to this service (ie. custom) - errno = 1 // generic error, check logs / windows events - } - - // wait until everything else is finished - finishWg.Wait() - // send stopped status - changes <- svc.Status{State: svc.Stopped} - // wait a little for the status to reach Windows - time.Sleep(100 * time.Millisecond) - - return ssec, errno -} - -func runService(_ *cobra.Command, opts *Options, cmdArgs []string) error { - // check if we are running interactively - isDebug, err := svc.IsAnInteractiveSession() - if err != nil { - return fmt.Errorf("could not determine if running interactively: %s", err) - } - // select service run type - svcRun := svc.Run - if isDebug { - log.Printf("WARNING: running interactively, switching to debug execution (no real service).\n") - svcRun = debug.Run - } - - runWg.Add(2) - finishWg.Add(1) - - // run service client - go func() { - sErr := svcRun(serviceName, &windowsService{}) - initiateShutdown(sErr) - runWg.Done() - }() - - // run service - go func() { - // run slightly delayed - time.Sleep(250 * time.Millisecond) - err := run(opts, getExecArgs(opts, cmdArgs)) - initiateShutdown(err) - finishWg.Done() - runWg.Done() - }() - - runWg.Wait() - - err = getShutdownError() - if err != nil { - log.Printf("%s service experienced an error: %s\n", serviceName, err) - } - - return err -} diff --git a/cmds/portmaster-start/show.go b/cmds/portmaster-start/show.go deleted file mode 100644 index 7ae6fc85..00000000 --- a/cmds/portmaster-start/show.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/service/updates/helper" -) - -func init() { - rootCmd.AddCommand(showCmd) - // sub-commands of show are registered using registerComponent -} - -var showCmd = &cobra.Command{ - Use: "show", - PersistentPreRunE: func(*cobra.Command, []string) error { - // All show sub-commands need the registry but no logging. - return configureRegistry(false) - }, - Short: "Show the command to run a Portmaster component yourself", -} - -func show(opts *Options, cmdArgs []string) error { - // get original arguments - args := getExecArgs(opts, cmdArgs) - - // adapt identifier - if onWindows { - opts.Identifier += exeSuffix - } - - file, err := registry.GetFile( - helper.PlatformIdentifier(opts.Identifier), - ) - if err != nil { - return fmt.Errorf("could not get component: %w", err) - } - - fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " ")) - - return nil -} diff --git a/cmds/portmaster-start/shutdown.go b/cmds/portmaster-start/shutdown.go deleted file mode 100644 index 31d34f96..00000000 --- a/cmds/portmaster-start/shutdown.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "sync" -) - -var ( - // startupComplete signals that the start procedure completed. - // The channel is not closed, just signaled once. - startupComplete = make(chan struct{}) - - // shuttingDown signals that we are shutting down. - // The channel will be closed, but may not be closed directly - only via initiateShutdown. - shuttingDown = make(chan struct{}) - - // shutdownError is protected by shutdownLock. - shutdownError error //nolint:unused,errname // Not what the linter thinks it is. Currently used on windows only. - shutdownLock sync.Mutex -) - -func initiateShutdown(err error) { - shutdownLock.Lock() - defer shutdownLock.Unlock() - - select { - case <-shuttingDown: - return - default: - shutdownError = err - close(shuttingDown) - } -} - -func isShuttingDown() bool { - select { - case <-shuttingDown: - return true - default: - return false - } -} - -//nolint:deadcode,unused // false positive on linux, currently used by windows only -func getShutdownError() error { - shutdownLock.Lock() - defer shutdownLock.Unlock() - - return shutdownError -} diff --git a/cmds/portmaster-start/update.go b/cmds/portmaster-start/update.go deleted file mode 100644 index 544047e1..00000000 --- a/cmds/portmaster-start/update.go +++ /dev/null @@ -1,158 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/spf13/cobra" - - portlog "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - reset bool - intelOnly bool -) - -func init() { - rootCmd.AddCommand(updateCmd) - rootCmd.AddCommand(purgeCmd) - - flags := updateCmd.Flags() - flags.BoolVar(&reset, "reset", false, "Delete all resources and re-download the basic set") - flags.BoolVar(&intelOnly, "intel-only", false, "Only make downloading intel updates mandatory") -} - -var ( - updateCmd = &cobra.Command{ - Use: "update", - Short: "Run a manual update process", - RunE: func(cmd *cobra.Command, args []string) error { - return downloadUpdates() - }, - } - - purgeCmd = &cobra.Command{ - Use: "purge", - Short: "Remove old resource versions that are superseded by at least three versions", - RunE: func(cmd *cobra.Command, args []string) error { - return purge() - }, - } -) - -func indexRequired(cmd *cobra.Command) bool { - switch cmd { - case updateCmd, purgeCmd: - return true - default: - return false - } -} - -func downloadUpdates() error { - // Check if only intel data is mandatory. - if intelOnly { - helper.IntelOnly() - } - - // Set registry state notify callback. - registry.StateNotifyFunc = logProgress - - // Set required updates. - registry.MandatoryUpdates = helper.MandatoryUpdates() - registry.AutoUnpack = helper.AutoUnpackUpdates() - - if reset { - // Delete storage. - err := os.RemoveAll(registry.StorageDir().Path) - if err != nil { - return fmt.Errorf("failed to reset update dir: %w", err) - } - err = registry.StorageDir().Ensure() - if err != nil { - return fmt.Errorf("failed to create update dir: %w", err) - } - - // Reset registry resources. - registry.ResetResources() - } - - // Update all indexes. - err := registry.UpdateIndexes(context.TODO()) - if err != nil { - return err - } - - // Check if updates are available. - if len(registry.GetState().Updates.PendingDownload) == 0 { - log.Println("all resources are up to date") - return nil - } - - // Download all required updates. - err = registry.DownloadUpdates(context.TODO(), true) - if err != nil { - return err - } - - // Select versions and unpack the selected. - registry.SelectVersions() - err = registry.UnpackResources() - if err != nil { - return fmt.Errorf("failed to unpack resources: %w", err) - } - - if !intelOnly { - // Fix chrome-sandbox permissions - if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { - return fmt.Errorf("failed to fix electron permissions: %w", err) - } - } - - return nil -} - -func logProgress(state *updater.RegistryState) { - switch state.ID { - case updater.StateChecking: - if state.Updates.LastCheckAt == nil { - log.Println("checking for new versions") - } - case updater.StateDownloading: - if state.Details == nil { - log.Printf("downloading %d updates\n", len(state.Updates.PendingDownload)) - } else if downloadDetails, ok := state.Details.(*updater.StateDownloadingDetails); ok { - if downloadDetails.FinishedUpTo < len(downloadDetails.Resources) { - log.Printf( - "[%d/%d] downloading %s", - downloadDetails.FinishedUpTo+1, - len(downloadDetails.Resources), - downloadDetails.Resources[downloadDetails.FinishedUpTo], - ) - } else if state.Updates.LastDownloadAt == nil { - log.Println("finalizing downloads") - } - } - } -} - -func purge() error { - portlog.SetLogLevel(portlog.TraceLevel) - - // logging is configured as a persistent pre-run method inherited from - // the root command but since we don't use run.Run() we need to start - // logging ourself. - err := portlog.Start() - if err != nil { - fmt.Printf("failed to start logging: %s\n", err) - } - defer portlog.Shutdown() - - registry.Purge(3) - return nil -} diff --git a/cmds/portmaster-start/verify.go b/cmds/portmaster-start/verify.go deleted file mode 100644 index 9d63c51a..00000000 --- a/cmds/portmaster-start/verify.go +++ /dev/null @@ -1,179 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "io/fs" - "log" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/safing/jess" - "github.com/safing/jess/filesig" - portlog "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - verifyVerbose bool - verifyFix bool - - verifyCmd = &cobra.Command{ - Use: "verify", - Short: "Check integrity of updates / components", - RunE: func(cmd *cobra.Command, args []string) error { - return verifyUpdates(cmd.Context()) - }, - } -) - -func init() { - rootCmd.AddCommand(verifyCmd) - - flags := verifyCmd.Flags() - flags.BoolVarP(&verifyVerbose, "verbose", "v", false, "Enable verbose output") - flags.BoolVar(&verifyFix, "fix", false, "Delete and re-download broken components") -} - -func verifyUpdates(ctx context.Context) error { - // Force registry to require signatures for all enabled scopes. - for _, opts := range registry.Verification { - if opts != nil { - opts.DownloadPolicy = updater.SignaturePolicyRequire - opts.DiskLoadPolicy = updater.SignaturePolicyRequire - } - } - - // Load indexes again to ensure they are correctly signed. - err := registry.LoadIndexes(ctx) - if err != nil { - if verifyFix { - log.Println("[WARN] loading indexes failed, re-downloading...") - err = registry.UpdateIndexes(ctx) - if err != nil { - return fmt.Errorf("failed to download indexes: %w", err) - } - log.Println("[ OK ] indexes re-downloaded and verified") - } else { - return fmt.Errorf("failed to verify indexes: %w", err) - } - } else { - log.Println("[ OK ] indexes verified") - } - - // Verify all resources. - export := registry.Export() - var verified, fails, skipped int - for _, rv := range export { - for _, version := range rv.Versions { - // Don't verify files we don't have. - if !version.Available { - continue - } - - // Verify file signature. - file := version.GetFile() - fileData, err := file.Verify() - switch { - case err == nil: - verified++ - if verifyVerbose { - verifOpts := registry.GetVerificationOptions(file.Identifier()) - if verifOpts != nil { - log.Printf( - "[ OK ] valid signature for %s: signed by %s", - file.Path(), getSignedByMany(fileData, verifOpts.TrustStore), - ) - } else { - log.Printf("[ OK ] valid signature for %s", file.Path()) - } - } - - case errors.Is(err, updater.ErrVerificationNotConfigured): - skipped++ - if verifyVerbose { - log.Printf("[SKIP] no verification configured for %s", file.Path()) - } - - default: - log.Printf("[FAIL] failed to verify %s: %s", file.Path(), err) - fails++ - if verifyFix { - // Delete file. - err = os.Remove(file.Path()) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path(), err) - } else { - // We should not be changing the version, but we are in a cmd-like - // scenario here without goroutines. - version.Available = false - } - // Delete file sig. - err = os.Remove(file.Path() + filesig.Extension) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path()+filesig.Extension, err) - } else { - // We should not be changing the version, but we are in a cmd-like - // scenario here without goroutines. - version.SigAvailable = false - } - } - } - } - } - - if verified > 0 { - log.Printf("[STAT] verified %d files", verified) - } - if skipped > 0 && verifyVerbose { - log.Printf("[STAT] skipped %d files (no verification configured)", skipped) - } - if fails > 0 { - if verifyFix { - log.Printf("[WARN] verification failed on %d files, re-downloading...", fails) - } else { - return fmt.Errorf("failed to verify %d files", fails) - } - } else { - // Everything was verified! - return nil - } - - // Start logging system for update process. - portlog.SetLogLevel(portlog.InfoLevel) - err = portlog.Start() - if err != nil { - log.Printf("[WARN] failed to start logging for monitoring update process: %s\n", err) - } - defer portlog.Shutdown() - - // Re-download broken files. - registry.MandatoryUpdates = helper.MandatoryUpdates() - registry.AutoUnpack = helper.AutoUnpackUpdates() - err = registry.DownloadUpdates(ctx, true) - if err != nil { - return fmt.Errorf("failed to re-download files: %w", err) - } - - return nil -} - -func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string { - signedBy := make([]string, 0, len(fds)) - for _, fd := range fds { - if sig := fd.Signature(); sig != nil { - for _, seal := range sig.Signatures { - if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { - signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) - } else { - signedBy = append(signedBy, seal.ID) - } - } - } - } - return strings.Join(signedBy, " and ") -} diff --git a/cmds/portmaster-start/version.go b/cmds/portmaster-start/version.go deleted file mode 100644 index 6c28362a..00000000 --- a/cmds/portmaster-start/version.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime" - "sort" - "strings" - "text/tabwriter" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/info" -) - -var ( - showShortVersion bool - showAllVersions bool - versionCmd = &cobra.Command{ - Use: "version", - Short: "Display various portmaster versions", - Args: cobra.NoArgs, - PersistentPreRunE: func(*cobra.Command, []string) error { - if showAllVersions { - // If we are going to show all component versions, - // we need the registry to be configured. - if err := configureRegistry(false); err != nil { - return err - } - } - - return nil - }, - RunE: func(*cobra.Command, []string) error { - if !showAllVersions { - if showShortVersion { - fmt.Println(info.Version()) - return nil - } - - fmt.Println(info.FullVersion()) - return nil - } - - fmt.Printf("portmaster-start %s\n\n", info.Version()) - fmt.Printf("Assets:\n") - - all := registry.Export() - keys := make([]string, 0, len(all)) - for identifier := range all { - keys = append(keys, identifier) - } - sort.Strings(keys) - - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - for _, identifier := range keys { - res := all[identifier] - - if showShortVersion { - // in "short" mode, skip all resources that are irrelevant on that platform - if !strings.HasPrefix(identifier, "all") && !strings.HasPrefix(identifier, runtime.GOOS) { - continue - } - } - - fmt.Fprintf(tw, " %s\t%s\n", identifier, res.SelectedVersion.VersionNumber) - } - return tw.Flush() - }, - } -) - -func init() { - flags := versionCmd.Flags() - { - flags.BoolVar(&showShortVersion, "short", false, "Print only the version number.") - flags.BoolVar(&showAllVersions, "all", false, "Dump versions for all assets.") - } - - rootCmd.AddCommand(versionCmd) -} diff --git a/service/intel/geoip/init_test.go b/service/intel/geoip/init_test.go index b6d722dc..b56da4ea 100644 --- a/service/intel/geoip/init_test.go +++ b/service/intel/geoip/init_test.go @@ -21,7 +21,7 @@ type testInstance struct { var _ instance = &testInstance{} -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -54,6 +54,15 @@ func runTest(m *testing.M) error { return fmt.Errorf("failed to initialize dataroot: %w", err) } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "geoip_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } stub := &testInstance{} stub.db, err = dbmodule.New(stub) @@ -68,7 +77,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/netenv/init_test.go b/service/netenv/init_test.go index b747111b..1c026d68 100644 --- a/service/netenv/init_test.go +++ b/service/netenv/init_test.go @@ -21,7 +21,7 @@ type testInstance struct { var _ instance = &testInstance{} -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -54,6 +54,15 @@ func runTest(m *testing.M) error { return fmt.Errorf("failed to initialize dataroot: %w", err) } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "netenv_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } stub := &testInstance{} stub.db, err = dbmodule.New(stub) @@ -68,7 +77,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/profile/endpoints/endpoints_test.go b/service/profile/endpoints/endpoints_test.go index bbc81f6a..93219473 100644 --- a/service/profile/endpoints/endpoints_test.go +++ b/service/profile/endpoints/endpoints_test.go @@ -27,7 +27,7 @@ type testInstance struct { geoip *geoip.GeoIP } -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -61,6 +61,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "endpoints_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} stub.db, err = dbmodule.New(stub) if err != nil { @@ -74,7 +84,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/resolver/main_test.go b/service/resolver/main_test.go index 4efc8eb8..bc780e40 100644 --- a/service/resolver/main_test.go +++ b/service/resolver/main_test.go @@ -26,9 +26,7 @@ type testInstance struct { netenv *netenv.NetEnv } -// var _ instance = &testInstance{} - -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -70,6 +68,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "resolver_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} stub.db, err = dbmodule.New(stub) if err != nil { @@ -91,7 +99,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create netenv: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/updates/assets/portmaster.service b/service/updates/assets/portmaster.service deleted file mode 100644 index c69a9ff5..00000000 --- a/service/updates/assets/portmaster.service +++ /dev/null @@ -1,44 +0,0 @@ -[Unit] -Description=Portmaster by Safing -Documentation=https://safing.io -Documentation=https://docs.safing.io -Before=nss-lookup.target network.target shutdown.target -After=systemd-networkd.service -Conflicts=shutdown.target -Conflicts=firewalld.service -Wants=nss-lookup.target - -[Service] -Type=simple -Restart=on-failure -RestartSec=10 -LockPersonality=yes -MemoryDenyWriteExecute=yes -NoNewPrivileges=yes -PrivateTmp=yes -PIDFile=/opt/safing/portmaster/core-lock.pid -Environment=LOGLEVEL=info -Environment=PORTMASTER_ARGS= -EnvironmentFile=-/etc/default/portmaster -ProtectSystem=true -#ReadWritePaths=/var/lib/portmaster -#ReadWritePaths=/run/xtables.lock -RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 -RestrictNamespaces=yes -# In future version portmaster will require access to user home -# directories to verify application permissions. -ProtectHome=read-only -ProtectKernelTunables=yes -ProtectKernelLogs=yes -ProtectControlGroups=yes -PrivateDevices=yes -AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon -CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon -# SystemCallArchitectures=native -# SystemCallFilter=@system-service @module -# SystemCallErrorNumber=EPERM -ExecStart=/opt/safing/portmaster/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS -ExecStopPost=-/opt/safing/portmaster/portmaster-start recover-iptables - -[Install] -WantedBy=multi-user.target diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go index 9b796880..3f82e367 100644 --- a/service/updates/bundlegeneration.go +++ b/service/updates/bundlegeneration.go @@ -3,6 +3,7 @@ package updates import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "os" "path" @@ -179,3 +180,50 @@ func getIdentifierAndVersion(versionedPath string) (identifier, version string, // `dirPath + filename` is guaranteed by path.Split() return dirPath + filename, version, true } + +// GenerateMockFolder generates mock bundle folder for testing. +func GenerateMockFolder(dir, name, version string) error { + // Make sure dir exists + _ = os.MkdirAll(dir, defaultDirMode) + + // Create empty files + file, err := os.Create(filepath.Join(dir, "portmaster")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "portmaster-core")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "portmaster.zip")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "assets.zip")) + if err != nil { + return err + } + _ = file.Close() + + bundle, err := GenerateBundleFromDir(dir, BundleFileSettings{ + Name: name, + Version: version, + }) + if err != nil { + return err + } + + bundleStr, err := json.MarshalIndent(bundle, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal bundle: %s\n", err) + } + + err = os.WriteFile(filepath.Join(dir, "index.json"), bundleStr, defaultFileMode) + if err != nil { + return err + } + return nil +} diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 130265be..4087964b 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -40,8 +40,10 @@ func CreateDownloader(index UpdateIndex) Downloader { func (d *Downloader) downloadIndexFile(ctx context.Context) error { // Make sure dir exists - _ = os.MkdirAll(d.dir, defaultDirMode) - var err error + err := os.MkdirAll(d.dir, defaultDirMode) + if err != nil { + return fmt.Errorf("failed to create directory for updates: %s", d.dir) + } var content string for _, url := range d.indexURLs { content, err = d.downloadIndexFileFromURL(ctx, url) @@ -50,15 +52,15 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { continue } // Downloading was successful. - - bundle, err := ParseBundle(content) + var bundle *Bundle + bundle, err = ParseBundle(content) if err != nil { log.Warningf("updates: %s", err) continue } // Parsing was successful - - version, err := semver.NewVersion(d.bundle.Version) + var version *semver.Version + version, err = semver.NewVersion(d.bundle.Version) if err != nil { log.Warningf("updates: failed to parse bundle version: %s", err) continue @@ -79,7 +81,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { indexFilepath := filepath.Join(d.dir, d.indexFile) err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode) if err != nil { - return fmt.Errorf("failed to write index file: %s", err) + return fmt.Errorf("failed to write index file: %w", err) } return nil diff --git a/service/updates/module.go b/service/updates/module.go index 5df50c66..44b1146f 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -6,8 +6,6 @@ import ( "runtime" "time" - "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" @@ -215,8 +213,6 @@ func (u *Updates) Stop() error { } type instance interface { - API() *api.API - Config() *config.Config Restart() Shutdown() Notifications() *notifications.Notifications diff --git a/service/updates/updates_test.go b/service/updates/updates_test.go new file mode 100644 index 00000000..d2857aa5 --- /dev/null +++ b/service/updates/updates_test.go @@ -0,0 +1,89 @@ +package updates + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/safing/portmaster/base/notifications" +) + +type testInstance struct{} + +func (i *testInstance) Restart() {} +func (i *testInstance) Shutdown() {} + +func (i *testInstance) Notifications() *notifications.Notifications { + return nil +} + +func (i *testInstance) Ready() bool { + return true +} + +func (i *testInstance) SetCmdLineOperation(f func() error) {} + +func TestPreformUpdate(t *testing.T) { + t.Parallel() + + // Initialize mock instance + stub := &testInstance{} + + // Make tmp dirs + installedDir, err := os.MkdirTemp("", "updates_current") + if err != nil { + panic(err) + } + defer func() { _ = os.RemoveAll(installedDir) }() + updateDir, err := os.MkdirTemp("", "updates_new") + if err != nil { + panic(err) + } + defer func() { _ = os.RemoveAll(updateDir) }() + purgeDir, err := os.MkdirTemp("", "updates_purge") + if err != nil { + panic(err) + } + defer func() { _ = os.RemoveAll(purgeDir) }() + + // Generate mock files + if err := GenerateMockFolder(installedDir, "Test", "1.0.0"); err != nil { + panic(err) + } + if err := GenerateMockFolder(updateDir, "Test", "1.0.1"); err != nil { + panic(err) + } + + // Create updater + updates, err := New(stub, "Test", UpdateIndex{ + Directory: installedDir, + DownloadDirectory: updateDir, + PurgeDirectory: purgeDir, + IndexFile: "index.json", + AutoApply: false, + NeedsRestart: false, + }) + if err != nil { + panic(err) + } + // Read and parse the index file + if err := updates.downloader.Verify(); err != nil { + panic(err) + } + // Try to apply the updates + err = updates.applyUpdates(nil) + if err != nil { + panic(err) + } + + // CHeck if the current version is now the new. + bundle, err := LoadBundle(filepath.Join(installedDir, "index.json")) + if err != nil { + panic(err) + } + + if bundle.Version != "1.0.1" { + panic(fmt.Errorf("expected version 1.0.1 found %s", bundle.Version)) + } +} diff --git a/spn/hub/hub_test.go b/spn/hub/hub_test.go index 391a61e7..d82bd6cc 100644 --- a/spn/hub/hub_test.go +++ b/spn/hub/hub_test.go @@ -24,7 +24,7 @@ type testInstance struct { base *base.Base } -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -62,6 +62,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "hub_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} // Init stub.db, err = dbmodule.New(stub) @@ -76,7 +86,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/spn/instance.go b/spn/instance.go index 69114d38..ed942933 100644 --- a/spn/instance.go +++ b/spn/instance.go @@ -48,11 +48,12 @@ type Instance struct { runtime *runtime.Runtime rng *rng.Rng - core *core.Core - updates *updates.Updates - geoip *geoip.GeoIP - netenv *netenv.NetEnv - filterLists *filterlists.FilterLists + core *core.Core + binaryUpdates *updates.Updates + intelUpdates *updates.Updates + geoip *geoip.GeoIP + netenv *netenv.NetEnv + filterLists *filterlists.FilterLists access *access.Access cabin *cabin.Cabin @@ -74,6 +75,14 @@ func New() (*Instance, error) { instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) + binaryUpdateIndex := updates.UpdateIndex{ + // FIXME: fill + } + + intelUpdateIndex := updates.UpdateIndex{ + // FIXME: fill + } + var err error // Base modules @@ -111,7 +120,11 @@ func New() (*Instance, error) { if err != nil { return instance, fmt.Errorf("create core module: %w", err) } - instance.updates, err = updates.New(instance) + instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex) + if err != nil { + return instance, fmt.Errorf("create updates module: %w", err) + } + instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex) if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } @@ -181,7 +194,8 @@ func New() (*Instance, error) { instance.rng, instance.core, - instance.updates, + instance.binaryUpdates, + instance.intelUpdates, instance.geoip, instance.netenv, @@ -255,9 +269,14 @@ func (i *Instance) Base() *base.Base { return i.base } -// Updates returns the updates module. -func (i *Instance) Updates() *updates.Updates { - return i.updates +// BinaryUpdates returns the updates module. +func (i *Instance) BinaryUpdates() *updates.Updates { + return i.binaryUpdates +} + +// IntelUpdates returns the updates module. +func (i *Instance) IntelUpdates() *updates.Updates { + return i.intelUpdates } // GeoIP returns the geoip module. diff --git a/spn/navigator/module_test.go b/spn/navigator/module_test.go index d02e1e5d..6ad2ea46 100644 --- a/spn/navigator/module_test.go +++ b/spn/navigator/module_test.go @@ -62,6 +62,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "geoip_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} log.SetLogLevel(log.DebugLevel) @@ -78,7 +88,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub, "Intel Test", updates.UpdateIndex{}) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } From 8b68243cc67ad4fef81b69bd779f83acbe85a1af Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Wed, 9 Oct 2024 11:44:59 +0300 Subject: [PATCH 31/62] [WIP] Add update from custom url functionality --- service/core/api.go | 16 +++++++ service/updates/downloader.go | 12 ++--- service/updates/module.go | 86 +++++++++++++++++++++++++++++------ 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/service/core/api.go b/service/core/api.go index bb63464b..ea4f18d1 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -133,6 +133,22 @@ func registerAPIEndpoints() error { return err } + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "updates/from-url", + WriteMethod: "POST", + Write: api.PermitAnyone, + ActionFunc: func(ar *api.Request) (string, error) { + err := module.instance.BinaryUpdates().UpdateFromURL(string(ar.InputData)) + if err != nil { + return err.Error(), err + } + return "upgrade triggered", nil + }, + Name: "Replace current version from the version supplied in the URL", + }); err != nil { + return err + } + return nil } diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 4087964b..84259901 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -48,7 +48,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { for _, url := range d.indexURLs { content, err = d.downloadIndexFileFromURL(ctx, url) if err != nil { - log.Warningf("updates: failed while downloading index file %s", err) + log.Warningf("updates: failed while downloading index file: %s", err) continue } // Downloading was successful. @@ -60,7 +60,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { } // Parsing was successful var version *semver.Version - version, err = semver.NewVersion(d.bundle.Version) + version, err = semver.NewVersion(bundle.Version) if err != nil { log.Warningf("updates: failed to parse bundle version: %s", err) continue @@ -116,7 +116,7 @@ func (d *Downloader) downloadIndexFileFromURL(ctx context.Context, url string) ( // Request the index file req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { - return "", fmt.Errorf("failed to create GET request to %s: %w", url, err) + return "", fmt.Errorf("failed to create GET request to: %w", err) } if UserAgent != "" { req.Header.Set("User-Agent", UserAgent) @@ -281,14 +281,14 @@ func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, erro // Try to make the request req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { - return nil, fmt.Errorf("failed to create GET request to %s: %s", url, err) + return nil, fmt.Errorf("failed to create GET request to %s: %w", url, err) } if UserAgent != "" { req.Header.Set("User-Agent", UserAgent) } resp, err := d.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("failed a get file request to: %s", err) + return nil, fmt.Errorf("failed a get file request to: %w", err) } defer func() { _ = resp.Body.Close() }() @@ -299,7 +299,7 @@ func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, erro content, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read body of response: %s", err) + return nil, fmt.Errorf("failed to read body of response: %w", err) } return content, nil } diff --git a/service/updates/module.go b/service/updates/module.go index 44b1146f..2e583a84 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -9,6 +9,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" + "github.com/tevino/abool" ) const ( @@ -48,7 +49,7 @@ type Updates struct { states *mgr.StateMgr updateCheckWorkerMgr *mgr.WorkerMgr - upgraderWorkerMgr *mgr.WorkerMgr + upgradeWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] @@ -58,6 +59,8 @@ type Updates struct { autoApply bool needsRestart bool + isUpdateRunning *abool.AtomicBool + instance instance } @@ -70,15 +73,25 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), - autoApply: index.AutoApply, - needsRestart: index.NeedsRestart, + autoApply: index.AutoApply, + needsRestart: index.NeedsRestart, + isUpdateRunning: abool.NewBool(false), instance: instance, } // Workers module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil).Repeat(updateTaskRepeatDuration) - module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", module.applyUpdates, nil) + module.upgradeWorkerMgr = m.NewWorkerMgr("upgrader", func(w *mgr.WorkerCtx) error { + if !module.isUpdateRunning.SetToIf(false, true) { + return fmt.Errorf("unable to apply updates, concurrent updater task is running") + } + // Make sure to unset it + defer module.isUpdateRunning.UnSet() + + module.applyUpdates(module.downloader, false) + return nil + }, nil) var err error module.registry, err = CreateRegistry(index) @@ -92,11 +105,17 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { } func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { + if !u.isUpdateRunning.SetToIf(false, true) { + return fmt.Errorf("unable to check for updates, concurrent updater task is running") + } + // Make sure to unset it on return. + defer u.isUpdateRunning.UnSet() // Download the index file. err := u.downloader.downloadIndexFile(wc.Ctx()) if err != nil { return fmt.Errorf("failed to download index file: %w", err) } + // Check if there is a new version. if u.downloader.version.LessThanOrEqual(u.registry.version) { log.Infof("updates: check compete: no new updates") @@ -115,8 +134,8 @@ func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { log.Errorf("updates: failed to download update: %s", err) } else { if u.autoApply { - // Trigger upgrade. - u.upgraderWorkerMgr.Go() + // Apply updates. + u.applyUpdates(u.downloader, false) } else { // Notify the user with option to trigger upgrade. notifications.NotifyPrompt(updateAvailableNotificationID, "New update is available.", fmt.Sprintf("%s %s", downloadBundle.Name, downloadBundle.Version), notifications.Action{ @@ -133,16 +152,57 @@ func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { return nil } -func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error { - currentBundle := u.registry.bundle - downloadBundle := u.downloader.bundle - if u.downloader.version.LessThanOrEqual(u.registry.version) { - // No new version, silently return. +// UpdateFromURL installs an update from the provided url. +func (u *Updates) UpdateFromURL(url string) error { + if !u.isUpdateRunning.SetToIf(false, true) { + return fmt.Errorf("unable to upgrade from url, concurrent updater task is running") + } + + u.m.Go("custom-url-downloader", func(w *mgr.WorkerCtx) error { + // Make sure to unset it on return. + defer u.isUpdateRunning.UnSet() + + // Initialize parameters + index := UpdateIndex{ + DownloadDirectory: u.downloader.dir, + IndexURLs: []string{url}, + IndexFile: u.downloader.indexFile, + } + + // Initialize with proper values and download the index file. + downloader := CreateDownloader(index) + err := downloader.downloadIndexFile(w.Ctx()) + if err != nil { + return err + } + + // Start downloading the artifacts + err = downloader.downloadAndVerify(w.Ctx()) + if err != nil { + return err + } + + // Artifacts are downloaded, perform the update. + u.applyUpdates(downloader, true) + return nil + }) + return nil +} + +func (u *Updates) applyUpdates(downloader Downloader, force bool) error { + currentBundle := u.registry.bundle + downloadBundle := downloader.bundle + + if !force { + if u.downloader.version.LessThanOrEqual(u.registry.version) { + // No new version, silently return. + return nil + } } log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) - err := u.registry.performRecoverableUpgrade(u.downloader.dir, u.downloader.indexFile) + err := u.registry.performRecoverableUpgrade(downloader.dir, downloader.indexFile) if err != nil { // Notify the user that update failed. notifications.NotifyPrompt(updateFailedNotificationID, "Failed to apply update.", err.Error()) @@ -166,7 +226,7 @@ func (u *Updates) TriggerUpdateCheck() { // TriggerApplyUpdates triggers upgrade. func (u *Updates) TriggerApplyUpdates() { - u.upgraderWorkerMgr.Go() + u.upgradeWorkerMgr.Go() } // States returns the state manager. From 5d9088f27ed05553042271e082fbfbfb679e777e Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 9 Oct 2024 16:50:46 +0200 Subject: [PATCH 32/62] [WIP] Improve bundle generation --- cmds/updatemgr/main.go | 62 ++++--- go.mod | 1 + go.sum | 2 + service/updates/bundle.go | 2 + service/updates/bundlegeneration.go | 273 +++++++++++++++++++++++----- 5 files changed, 271 insertions(+), 69 deletions(-) diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go index a157af07..1d998581 100644 --- a/cmds/updatemgr/main.go +++ b/cmds/updatemgr/main.go @@ -9,44 +9,54 @@ import ( "github.com/safing/portmaster/service/updates" ) -var binaryMap = map[string]updates.Artifact{ - "geoipv4.mmdb.gz": { - Filename: "geoipv4.mmdb", - Unpack: "gz", - }, - "geoipv6.mmdb.gz": { - Filename: "geoipv6.mmdb", - Unpack: "gz", - }, -} +var bundleSettings = updates.BundleFileSettings{ + Name: "Portmaster Binaries", + PrimaryArtifact: "linux_amd64/portmaster-core", + BaseURL: "https://updates.safing.io/", + IgnoreFiles: []string{ + // Indexes, checksums, latest symlinks. + "*.json", + "sha256*.txt", + "latest/**", -var ignoreFiles = map[string]struct{}{ - "bin-index.json": {}, - "intel-index.json": {}, + // Signatures. + "*.sig", + "**/*.sig", + + // Related, but not required artifacts. + "**/*.apk", + "**/*install*", + "**/spn-hub*", + "**/jess*", + "**/hubs*.json", + "**/*mini*.mmdb.gz", + + // Deprecated artifacts. + "**/profilemgr*.zip", + "**/settings*.zip", + "**/monitor*.zip", + "**/base*.zip", + "**/console*.zip", + "**/portmaster-wintoast*.dll", + "**/portmaster-snoretoast*.exe", + "**/portmaster-kext*.dll", + }, + UnpackFiles: map[string]string{ + "gz": "**/*.gz", + "zip": "**/app2/**/portmaster-app*.zip", + }, } func main() { dir := flag.String("dir", "", "path to the directory that contains the artifacts") - name := flag.String("name", "", "name of the bundle") - version := flag.String("version", "", "version of the bundle") flag.Parse() if *dir == "" { fmt.Fprintf(os.Stderr, "-dir parameter is required\n") return } - if *name == "" { - fmt.Fprintf(os.Stderr, "-name parameter is required\n") - return - } - settings := updates.BundleFileSettings{ - Name: *name, - Version: *version, - Properties: binaryMap, - IgnoreFiles: ignoreFiles, - } - bundle, err := updates.GenerateBundleFromDir(*dir, settings) + bundle, err := updates.GenerateBundleFromDir(*dir, bundleSettings) if err != nil { fmt.Fprintf(os.Stderr, "failed to generate bundle: %s\n", err) return diff --git a/go.mod b/go.mod index 436df094..9590ae56 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect diff --git a/go.sum b/go.sum index 50df69d6..fbd91dd0 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,8 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 64ffc2a1..deee6666 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -24,6 +24,8 @@ type Artifact struct { Platform string `json:"Platform,omitempty"` Unpack string `json:"Unpack,omitempty"` Version string `json:"Version,omitempty"` + + localFile string } func (a *Artifact) GetFileMode() os.FileMode { diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go index 3f82e367..b7117926 100644 --- a/service/updates/bundlegeneration.go +++ b/service/updates/bundlegeneration.go @@ -4,94 +4,281 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" + "io/fs" "os" "path" "path/filepath" "regexp" + "slices" "strings" "time" + "github.com/gobwas/glob" semver "github.com/hashicorp/go-version" ) type BundleFileSettings struct { - Name string - Version string - Properties map[string]Artifact - IgnoreFiles map[string]struct{} + Name string + Version string + PrimaryArtifact string + BaseURL string + + Templates map[string]Artifact + IgnoreFiles []string + UnpackFiles map[string]string + + cleanedBaseURL string + ignoreFilesGlobs []glob.Glob + unpackFilesGlobs map[string]glob.Glob +} + +func (bs *BundleFileSettings) init() error { + // Transform base URL into expected format. + bs.cleanedBaseURL = strings.TrimSuffix(bs.BaseURL, "/") + "/" + + // Parse ignore files patterns. + bs.ignoreFilesGlobs = make([]glob.Glob, 0, len(bs.IgnoreFiles)) + for _, pattern := range bs.IgnoreFiles { + g, err := glob.Compile(pattern, os.PathSeparator) + if err != nil { + return fmt.Errorf("invalid ingore files pattern %q: %w", pattern, err) + } + bs.ignoreFilesGlobs = append(bs.ignoreFilesGlobs, g) + } + + // Parse unpack files patterns. + bs.unpackFilesGlobs = make(map[string]glob.Glob) + for setting, pattern := range bs.UnpackFiles { + g, err := glob.Compile(pattern, os.PathSeparator) + if err != nil { + return fmt.Errorf("invalid unpack files pattern %q: %w", pattern, err) + } + bs.unpackFilesGlobs[setting] = g + } + + return nil +} + +// IsIgnored returns whether a filename should be ignored. +func (bs *BundleFileSettings) IsIgnored(filename string) bool { + for _, ignoreGlob := range bs.ignoreFilesGlobs { + if ignoreGlob.Match(filename) { + return true + } + } + + return false +} + +// UnpackSetting returns the unpack setings for the given filename. +func (bs *BundleFileSettings) UnpackSetting(filename string) (string, error) { + var foundSetting string + +settings: + for unpackSetting, matchGlob := range bs.unpackFilesGlobs { + switch { + case !matchGlob.Match(filename): + // Check next if glob does not match. + continue settings + case foundSetting == "": + // First find, save setting. + foundSetting = unpackSetting + case foundSetting != unpackSetting: + // Additional find, and setting is not the same. + return "", errors.New("matches contradicting unpack settings") + } + } + + return foundSetting, nil } // GenerateBundleFromDir generates a bundle from a given folder. func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bundle, error) { - bundleDirName := filepath.Base(bundleDir) + artifacts := make(map[string]Artifact) - artifacts := make([]Artifact, 0, 5) - err := filepath.Walk(bundleDir, func(path string, info os.FileInfo, err error) error { + // Initialize. + err := settings.init() + if err != nil { + return nil, fmt.Errorf("invalid bundle settings: %w", err) + } + bundleDir, err = filepath.Abs(bundleDir) + if err != nil { + return nil, fmt.Errorf("invalid bundle dir: %w", err) + } + + err = filepath.WalkDir(bundleDir, func(fullpath string, d fs.DirEntry, err error) error { + // Fail on access error. if err != nil { return err } - // Skip folders - if info.IsDir() { + + // Step 1: Extract information and check ignores. + + // Skip folders. + if d.IsDir() { return nil } - identifier, version, ok := getIdentifierAndVersion(info.Name()) - if !ok { - identifier = info.Name() + // Get relative path for processing. + relpath, err := filepath.Rel(bundleDir, fullpath) + if err != nil { + return fmt.Errorf("invalid relative path for %s: %w", fullpath, err) } // Check if file is in the ignore list. - if _, ok := settings.IgnoreFiles[identifier]; ok { + if settings.IsIgnored(relpath) { return nil } - artifact := Artifact{} - - // Check if the caller provided properties for the artifact. - if p, ok := settings.Properties[identifier]; ok { - artifact = p + // Extract version, if present. + identifier, version, ok := getIdentifierAndVersion(d.Name()) + if !ok { + // Fallback to using filename as identifier, which is normal for the simplified system. + identifier = d.Name() + version = "" + } + var versionNum *semver.Version + if version != "" { + versionNum, err = semver.NewVersion(version) + if err != nil { + return fmt.Errorf("invalid version %s for %s: %w", relpath, version, err) + } } - // Set filename of artifact if not set by the caller. + // Extract platform. + platform := "all" + before, _, found := strings.Cut(relpath, string(os.PathSeparator)) + if found { + platform = before + } + + // Step 2: Check and compare file version. + + // Make the key platform specific since there can be same filename for multiple platforms. + key := platform + "/" + identifier + existing, ok := artifacts[key] + if ok { + // Check for duplicates and mixed versioned/non-versioned. + switch { + case existing.Version == version: + return fmt.Errorf("duplicate version for %s: %s and %s", key, existing.localFile, fullpath) + case (existing.Version == "") != (version == ""): + return fmt.Errorf("both a versioned and non-versioned file for: %s: %s and %s", key, existing.localFile, fullpath) + } + + // Compare versions. + existingVersion, _ := semver.NewVersion(existing.Version) + switch { + case existingVersion.Equal(versionNum): + return fmt.Errorf("duplicate version for %s: %s and %s", key, existing.localFile, fullpath) + case existingVersion.GreaterThan(versionNum): + // New version is older, skip. + return nil + } + } + + // Step 3: Create new Artifact. + + artifact := Artifact{} + + // Check if the caller provided a template for the artifact. + if t, ok := settings.Templates[identifier]; ok { + artifact = t + } + + // Set artifact properties. if artifact.Filename == "" { artifact.Filename = identifier } - - artifact.Version = version - - // Fill the platform of the artifact - parentDir := filepath.Base(filepath.Dir(path)) - if parentDir != "all" && parentDir != bundleDirName { - artifact.Platform = parentDir + if len(artifact.URLs) == 0 && settings.BaseURL != "" { + artifact.URLs = []string{settings.cleanedBaseURL + relpath} + } + if artifact.Platform == "" { + artifact.Platform = platform + } + if artifact.Unpack == "" { + unpackSetting, err := settings.UnpackSetting(relpath) + if err != nil { + return fmt.Errorf("invalid unpack setting for %s at %s: %w", key, relpath, err) + } + artifact.Unpack = unpackSetting + } + if artifact.Version == "" { + artifact.Version = version } - // Fill the hash - hash, err := getSHA256(path, artifact.Unpack) - if err != nil { - return fmt.Errorf("failed to calculate hash of file: %s %w", path, err) - } - artifact.SHA256 = hash + // Set local file path. + artifact.localFile = fullpath - artifacts = append(artifacts, artifact) + // Save new artifact to map. + artifacts[key] = artifact return nil }) if err != nil { - return nil, fmt.Errorf("failed to walk the dir: %w", err) + return nil, fmt.Errorf("scanning dir: %w", err) } - // Filter artifact so we have single version for each file - artifacts, err = selectLatestArtifacts(artifacts) - if err != nil { - return nil, fmt.Errorf("failed to select artifact version: %w", err) - } - - return &Bundle{ + // Create base bundle. + bundle := &Bundle{ Name: settings.Name, Version: settings.Version, - Artifacts: artifacts, Published: time.Now(), - }, nil + } + if bundle.Version == "" && settings.PrimaryArtifact != "" { + pv, ok := artifacts[settings.PrimaryArtifact] + if ok { + bundle.Version = pv.Version + } + } + if bundle.Name == "" { + bundle.Name = strings.Trim(filepath.Base(bundleDir), "./\\") + } + + // Convert to slice and compute hashes. + export := make([]Artifact, 0, len(artifacts)) + for _, artifact := range artifacts { + // Compute hash. + hash, err := getSHA256(artifact.localFile, artifact.Unpack) + if err != nil { + return nil, fmt.Errorf("calculate hash of file: %s %w", artifact.localFile, err) + } + artifact.SHA256 = hash + + // Remove "all" platform IDs. + if artifact.Platform == "all" { + artifact.Platform = "" + } + + // Remove default versions. + if artifact.Version == bundle.Version { + artifact.Version = "" + } + + // Add to export slice. + export = append(export, artifact) + } + + // Sort final artifacts. + slices.SortFunc(export, func(a, b Artifact) int { + switch { + case a.Filename != b.Filename: + return strings.Compare(a.Filename, b.Filename) + case a.Platform != b.Platform: + return strings.Compare(a.Platform, b.Platform) + case a.Version != b.Version: + return strings.Compare(a.Version, b.Version) + case a.SHA256 != b.SHA256: + return strings.Compare(a.SHA256, b.SHA256) + default: + return 0 + } + }) + + // Assign and return. + bundle.Artifacts = export + return bundle, nil } func selectLatestArtifacts(artifacts []Artifact) ([]Artifact, error) { From a1a4bf6325d6b0761ddc11cf28d4c5c643a16a5d Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 10 Oct 2024 08:58:02 +0200 Subject: [PATCH 33/62] [WIP] Use cobra for updatemgr --- cmds/updatemgr/main.go | 65 ++++------------------------------- cmds/updatemgr/scan.go | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 58 deletions(-) create mode 100644 cmds/updatemgr/scan.go diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go index 1d998581..e7703d77 100644 --- a/cmds/updatemgr/main.go +++ b/cmds/updatemgr/main.go @@ -1,71 +1,20 @@ package main import ( - "encoding/json" - "flag" "fmt" "os" - "github.com/safing/portmaster/service/updates" + "github.com/spf13/cobra" ) -var bundleSettings = updates.BundleFileSettings{ - Name: "Portmaster Binaries", - PrimaryArtifact: "linux_amd64/portmaster-core", - BaseURL: "https://updates.safing.io/", - IgnoreFiles: []string{ - // Indexes, checksums, latest symlinks. - "*.json", - "sha256*.txt", - "latest/**", - - // Signatures. - "*.sig", - "**/*.sig", - - // Related, but not required artifacts. - "**/*.apk", - "**/*install*", - "**/spn-hub*", - "**/jess*", - "**/hubs*.json", - "**/*mini*.mmdb.gz", - - // Deprecated artifacts. - "**/profilemgr*.zip", - "**/settings*.zip", - "**/monitor*.zip", - "**/base*.zip", - "**/console*.zip", - "**/portmaster-wintoast*.dll", - "**/portmaster-snoretoast*.exe", - "**/portmaster-kext*.dll", - }, - UnpackFiles: map[string]string{ - "gz": "**/*.gz", - "zip": "**/app2/**/portmaster-app*.zip", - }, +var rootCmd = &cobra.Command{ + Use: "updatemgr", + Short: "Manage update artifacts.", } func main() { - dir := flag.String("dir", "", "path to the directory that contains the artifacts") - - flag.Parse() - if *dir == "" { - fmt.Fprintf(os.Stderr, "-dir parameter is required\n") - return + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) } - - bundle, err := updates.GenerateBundleFromDir(*dir, bundleSettings) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to generate bundle: %s\n", err) - return - } - - bundleStr, err := json.MarshalIndent(&bundle, "", " ") - if err != nil { - fmt.Fprintf(os.Stderr, "failed to marshal bundle: %s\n", err) - } - - fmt.Printf("%s", bundleStr) } diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go new file mode 100644 index 00000000..fde03eff --- /dev/null +++ b/cmds/updatemgr/scan.go @@ -0,0 +1,78 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/safing/portmaster/service/updates" + "github.com/spf13/cobra" +) + +var ( + bundleSettings = updates.BundleFileSettings{ + Name: "Portmaster Binaries", + PrimaryArtifact: "linux_amd64/portmaster-core", + BaseURL: "https://updates.safing.io/", + IgnoreFiles: []string{ + // Indexes, checksums, latest symlinks. + "*.json", + "sha256*.txt", + "latest/**", + + // Signatures. + "*.sig", + "**/*.sig", + + // Related, but not required artifacts. + "**/*.apk", + "**/*install*", + "**/spn-hub*", + "**/jess*", + "**/hubs*.json", + "**/*mini*.mmdb.gz", + + // Deprecated artifacts. + "**/profilemgr*.zip", + "**/settings*.zip", + "**/monitor*.zip", + "**/base*.zip", + "**/console*.zip", + "**/portmaster-wintoast*.dll", + "**/portmaster-snoretoast*.exe", + "**/portmaster-kext*.dll", + }, + UnpackFiles: map[string]string{ + "gz": "**/*.gz", + "zip": "**/app2/**/portmaster-app*.zip", + }, + } + + scanCmd = &cobra.Command{ + Use: "scan", + Short: "Scans the contents of the specified directory and creates an index from it.", + RunE: scan, + } + + bundleDir string +) + +func init() { + rootCmd.AddCommand(scanCmd) + scanCmd.Flags().StringVarP(&bundleDir, "dir", "d", "", "directory to create index from (required)") + _ = scanCmd.MarkFlagRequired("dir") +} + +func scan(cmd *cobra.Command, args []string) error { + bundle, err := updates.GenerateBundleFromDir(bundleDir, bundleSettings) + if err != nil { + return err + } + + bundleStr, err := json.MarshalIndent(&bundle, "", " ") + if err != nil { + return fmt.Errorf("marshal index: %w", err) + } + + fmt.Printf("%s", bundleStr) + return nil +} From 02791a4d4232897645519c735298abb3d2ff728b Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 10 Oct 2024 13:02:13 +0300 Subject: [PATCH 34/62] [WIP] Add fallback on corrupted install --- service/updates/module.go | 29 +++++++++++++++++++++++------ service/updates/registry.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/service/updates/module.go b/service/updates/module.go index 2e583a84..f33c830c 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -13,9 +13,10 @@ import ( ) const ( - updateTaskRepeatDuration = 1 * time.Hour - updateAvailableNotificationID = "updates:update-available" - updateFailedNotificationID = "updates:update-failed" + updateTaskRepeatDuration = 1 * time.Hour + updateAvailableNotificationID = "updates:update-available" + updateFailedNotificationID = "updates:update-failed" + corruptInstallationNotificationID = "updates:corrupt-installation" // ResourceUpdateEvent is emitted every time the // updater successfully performed a resource update. @@ -59,6 +60,8 @@ type Updates struct { autoApply bool needsRestart bool + corruptedInstallation bool + isUpdateRunning *abool.AtomicBool instance instance @@ -96,7 +99,14 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) { var err error module.registry, err = CreateRegistry(index) if err != nil { - return nil, fmt.Errorf("failed to create registry: %w", err) + // Installation is corrupt, set flag and fall back to folder scanning for artifacts discovery. + log.Criticalf("updates: failed to create registry: %s (falling back to folder scanning)", err) + module.corruptedInstallation = true + + module.registry, err = CreateRegistryFromFolder(index) + if err != nil { + return nil, err + } } module.downloader = CreateDownloader(index) @@ -194,14 +204,16 @@ func (u *Updates) applyUpdates(downloader Downloader, force bool) error { currentBundle := u.registry.bundle downloadBundle := downloader.bundle - if !force { + if !force && u.registry.version != nil { if u.downloader.version.LessThanOrEqual(u.registry.version) { // No new version, silently return. return nil } } + if currentBundle != nil { + log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) + } - log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) err := u.registry.performRecoverableUpgrade(downloader.dir, downloader.indexFile) if err != nil { // Notify the user that update failed. @@ -247,6 +259,11 @@ func (u *Updates) Start() error { _ = u.downloader.deleteUnfinishedDownloads() return nil }) + + if u.corruptedInstallation { + notifications.NotifyError(corruptInstallationNotificationID, "Corrupted installation. Reinstall the software.", "") + } + u.updateCheckWorkerMgr.Go() return nil diff --git a/service/updates/registry.go b/service/updates/registry.go index 9b07a742..98df62a0 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "strings" semver "github.com/hashicorp/go-version" @@ -54,6 +55,35 @@ func CreateRegistry(index UpdateIndex) (Registry, error) { return registry, nil } +func CreateRegistryFromFolder(index UpdateIndex) (Registry, error) { + registry := Registry{ + dir: index.Directory, + purgeDir: index.PurgeDirectory, + files: make(map[string]File), + } + + files, err := os.ReadDir(index.Directory) + if err != nil { + return Registry{}, nil + } + for _, file := range files { + // Skip dirs + if file.IsDir() { + continue + } + + // Skip the uninstaller. (Windows) + if strings.Contains(strings.ToLower(file.Name()), "uninstall") { + continue + } + + artifactPath := filepath.Join(registry.dir, file.Name()) + registry.files[file.Name()] = File{id: file.Name(), path: artifactPath, version: "", sha256: ""} + } + + return registry, nil +} + func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { // Make sure provided update is valid indexFilepath := filepath.Join(downloadDir, indexFile) From dd643a29d40098b79fc4db1a49bc318288cc1005 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 10:38:00 +0300 Subject: [PATCH 35/62] [WIP] Github workflow for the installers --- .github/workflows/release.yml | 74 +++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..354e0e43 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: Release + +on: + workflow_dispatch: + +jobs: + release-prep: + name: Prep + runs-on: ubuntu-latest + steps: + - uses: earthly/actions-setup@v1 + with: + version: v0.8.0 + - uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build all artifacts + run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +release-prep + + - name: Upload Dist + uses: actions/upload-artifact@v2 + with: + name: dist + path: dist/ + + installer-linux: + name: Installer linux + runs-on: ubuntu-latest + needs: release-prep + steps: + - uses: earthly/actions-setup@v1 + with: + version: v0.8.0 + - uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build linux installers + run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +installer-linux + + installer-windows: + name: Installer windows + runs-on: windows-latest + needs: release-prep + container: + image: abrarov/msvc-2022:latest + steps: + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Download Dist + uses: actions/download-artifact@v2 + with: + name: dist + path: dist/ + + - name: Build all artifacts + run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 + From a74720b019de9aa635192006f22b3d2db27e34d1 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 10:45:18 +0300 Subject: [PATCH 36/62] [WIP] Update release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 354e0e43..e7e74037 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,6 +69,6 @@ jobs: name: dist path: dist/ - - name: Build all artifacts + - name: Build windows artifacts run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 From 61a5b8027e430e8cf88563e37fed0a0aba7e9755 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 10:50:31 +0300 Subject: [PATCH 37/62] [WIP] Update release workflow --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7e74037..b8572d8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ name: Release -on: - workflow_dispatch: +on: workflow_dispatch jobs: release-prep: From a7d97fec553bc331cd9e3e436051ae3be6b21dbd Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 11:03:05 +0300 Subject: [PATCH 38/62] [WIP] Update release workflow --- .github/workflows/release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8572d8d..7748fb8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,10 @@ name: Release -on: workflow_dispatch +on: + push: + paths: + - '.github/workflows/release.yml' + workflow_dispatch: jobs: release-prep: From 7a28e40ccbc4d0d72456a605c6a1c3af1b7f2055 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 11:05:12 +0300 Subject: [PATCH 39/62] [WIP] Update release workflow --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7748fb8e..0d0850c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +release-prep - name: Upload Dist - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: dist path: dist/ @@ -67,7 +67,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Download Dist - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: dist path: dist/ From 96b1280b5c03fbdceb09eb294dcd0e246cb08dc1 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 11:40:28 +0300 Subject: [PATCH 40/62] [WIP] Fix tauri compile issue --- desktop/tauri/src-tauri/Cargo.lock | 2387 ++++++++++++--- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 2701 +++++++---------- .../src-tauri/gen/schemas/linux-schema.json | 2701 +++++++---------- desktop/tauri/src-tauri/src/traymenu.rs | 6 +- 5 files changed, 4098 insertions(+), 3699 deletions(-) diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock index 0d3cdbaf..72551e9a 100644 --- a/desktop/tauri/src-tauri/Cargo.lock +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -215,6 +215,170 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "app-store-connect" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fb5489b9bfcfa3aec2f68cc79eafb999b5af9b9d9d70ca8dfe36acdd1b2b05" +dependencies = [ + "anyhow", + "base64 0.21.7", + "clap 4.5.17", + "dirs 5.0.1", + "env_logger 0.10.2", + "jsonwebtoken", + "log", + "pem", + "rand 0.8.5", + "reqwest 0.11.27", + "rsa", + "serde", + "serde_json", + "thiserror", + "x509-certificate", +] + +[[package]] +name = "apple-bundles" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb7c27ee2ca7826adfdc84228cd4c5a84ab57b0a11d269d1d7cd0615238e5a2" +dependencies = [ + "anyhow", + "plist", + "simple-file-manifest", + "walkdir", +] + +[[package]] +name = "apple-codesign" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329820aac7259ca0529d3cc21dd3b4c11651225dfce9e0ce25b121b23f923164" +dependencies = [ + "anyhow", + "app-store-connect", + "apple-bundles", + "apple-flat-package", + "apple-xar", + "aws-config", + "aws-sdk-s3", + "aws-smithy-http", + "aws-smithy-types", + "base64 0.21.7", + "bcder", + "bitflags 2.6.0", + "bytes", + "chrono", + "clap 4.5.17", + "cryptographic-message-syntax", + "der 0.7.9", + "dialoguer", + "difference", + "digest", + "dirs 5.0.1", + "elliptic-curve 0.13.8", + "env_logger 0.10.2", + "figment", + "filetime", + "glob", + "goblin", + "hex", + "log", + "md-5", + "minicbor", + "num-traits", + "object 0.32.2", + "oid-registry", + "once_cell", + "p12", + "p256 0.13.2", + "pem", + "pkcs1", + "pkcs8 0.10.2", + "plist", + "rand 0.8.5", + "rasn", + "rayon", + "regex", + "reqwest 0.11.27", + "ring", + "rsa", + "scroll", + "security-framework", + "security-framework-sys", + "semver", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "signature 2.2.0", + "simple-file-manifest", + "spake2", + "spki 0.7.3", + "subtle", + "tempfile", + "thiserror", + "tokio", + "tungstenite", + "uuid", + "walkdir", + "widestring", + "windows-sys 0.52.0", + "x509", + "x509-certificate", + "xml-rs", + "yasna", + "zeroize", + "zip 0.6.6", + "zip_structs", +] + +[[package]] +name = "apple-flat-package" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6adc520e05304de5ec383487786fa20e9c636fe972e59719cdd93621a2db6f1" +dependencies = [ + "apple-xar", + "cpio-archive", + "flate2", + "scroll", + "serde", + "serde-xml-rs", + "thiserror", +] + +[[package]] +name = "apple-xar" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844e00dc1e665b3cf0bba745aa9c6464292ca512db0c11384511586701eb0335" +dependencies = [ + "base64 0.21.7", + "bcder", + "bzip2", + "chrono", + "cryptographic-message-syntax", + "digest", + "flate2", + "log", + "md-5", + "rand 0.8.5", + "reqwest 0.11.27", + "scroll", + "serde", + "serde-xml-rs", + "sha1", + "sha2", + "signature 2.2.0", + "thiserror", + "url", + "x509-certificate", + "xml-rs", + "xz2", +] + [[package]] name = "ar" version = "0.9.0" @@ -238,7 +402,7 @@ checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" dependencies = [ "clipboard-win", "core-graphics 0.23.2", - "image 0.25.2", + "image", "log", "objc2", "objc2-app-kit", @@ -324,6 +488,65 @@ dependencies = [ "zbus 4.4.0", ] +[[package]] +name = "ashpd" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 4.4.0", +] + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "assert-unchecked" version = "0.1.2" @@ -582,6 +805,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -628,6 +860,381 @@ dependencies = [ "arrayvec 0.7.6", ] +[[package]] +name = "aws-config" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "hex", + "http 0.2.12", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f2a62020f3e06f9b352b2a23547f6e1d110b6bf1e18a6b588ae36114eaf6e2" +dependencies = [ + "ahash 0.8.11", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand 2.1.1", + "hex", + "hmac", + "http 0.2.12", + "http-body 0.4.6", + "lru", + "once_cell", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33ae899566f3d395cbf42858e433930682cc9c1889fa89318896082fef45efb" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f39c09e199ebd96b9f860b0fce4b6625f211e064ad7c8693b72ecf7ef03881e0" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d95f93a98130389eb6233b9d615249e543f6c24a68ca1f109af9ca5164a8765" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8db6904450bafe7473c6ca9123f88cc11089e41a025408f992db4e22d3be68" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "p256 0.11.1", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.60.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc32c", + "crc32fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand 2.1.1", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147100a7bea70fa20ef224a6bad700358305f5dc0f84649c53769761395b355b" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa 1.0.11", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.7.5" @@ -639,10 +1246,10 @@ dependencies = [ "base64 0.21.7", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-util", "itoa 1.0.11", "matchit", @@ -674,8 +1281,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -697,10 +1304,16 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide 0.7.4", - "object", + "object 0.36.4", "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base16ct" version = "0.2.0" @@ -725,6 +1338,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -813,6 +1436,16 @@ dependencies = [ "wyz", ] +[[package]] +name = "bitvec-nom2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0" +dependencies = [ + "bitvec", + "nom", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1040,6 +1673,22 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytesize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" + [[package]] name = "bzip2" version = "0.4.4" @@ -1140,9 +1789,9 @@ dependencies = [ [[package]] name = "cargo-mobile2" -version = "0.17.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ede7b4200c8794c5fe7bc25c93a8f1756b87d50968cc20def67f2618035f65" +checksum = "5c8052fc43184dc6c572437c2f8dae83e4ca9a5b27c790e269b90b080c1b9301" dependencies = [ "colored", "core-foundation 0.10.0", @@ -1225,6 +1874,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.1.16" @@ -1393,22 +2051,6 @@ dependencies = [ "digest", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation 0.1.2", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types 0.5.0", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.26.0" @@ -1417,7 +2059,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ "bitflags 2.6.0", "block", - "cocoa-foundation 0.2.0", + "cocoa-foundation", "core-foundation 0.10.0", "core-graphics 0.24.0", "foreign-types 0.5.0", @@ -1425,20 +2067,6 @@ dependencies = [ "objc", ] -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - [[package]] name = "cocoa-foundation" version = "0.2.0" @@ -1553,6 +2181,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_panic" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1565,6 +2199,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1654,6 +2297,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e3adec7390c7643049466136117057188edf5f23efc5c8b4fc8079c8dc34a6" +[[package]] +name = "cpio-archive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d5133d716d3d82da8c76367ddb0ab1733e2629f1462e4f39947e13b8b4b741" +dependencies = [ + "chrono", + "is_executable", + "simple-file-manifest", + "thiserror", +] + [[package]] name = "cpufeatures" version = "0.2.13" @@ -1669,6 +2324,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -1718,6 +2382,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1741,6 +2417,23 @@ dependencies = [ "typenum", ] +[[package]] +name = "cryptographic-message-syntax" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c324ba1028cef7e3a71a00cbf585637bb0215dec2f6a2b566d094190a1309b" +dependencies = [ + "bcder", + "bytes", + "chrono", + "hex", + "pem", + "reqwest 0.11.27", + "ring", + "signature 2.2.0", + "x509-certificate", +] + [[package]] name = "css-color" version = "0.2.8" @@ -1814,6 +2507,7 @@ dependencies = [ "curve25519-dalek-derive", "digest", "fiat-crypto", + "rand_core 0.6.4", "rustc_version", "subtle", "zeroize", @@ -1843,7 +2537,7 @@ dependencies = [ "objc", "rust-ini", "web-sys", - "winreg", + "winreg 0.52.0", "xdg", "zbus 3.15.2", ] @@ -1973,6 +2667,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der" version = "0.7.9" @@ -2094,6 +2798,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + [[package]] name = "digest" version = "0.10.7" @@ -2176,6 +2886,15 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.7.0" @@ -2208,6 +2927,18 @@ dependencies = [ "const-random", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.1" @@ -2226,10 +2957,10 @@ dependencies = [ "digest", "num-bigint-dig", "num-traits", - "pkcs8", - "rfc6979", + "pkcs8 0.10.2", + "rfc6979 0.4.0", "sha2", - "signature", + "signature 2.2.0", "zeroize", ] @@ -2285,18 +3016,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.9", "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -2305,8 +3048,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -2335,23 +3078,43 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest", - "ff", + "ff 0.13.0", "generic-array", - "group", + "group 0.13.0", "hkdf", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -2367,7 +3130,7 @@ dependencies = [ "rustc_version", "toml 0.8.19", "vswhom", - "winreg", + "winreg 0.52.0", ] [[package]] @@ -2456,6 +3219,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.5" @@ -2562,8 +3338,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" dependencies = [ "bit-set", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2599,6 +3375,16 @@ dependencies = [ "log", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.13.0" @@ -2625,6 +3411,20 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml 0.8.19", + "uncased", + "version_check", +] + [[package]] name = "filetime" version = "0.2.25" @@ -2677,6 +3477,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -3027,19 +3833,6 @@ dependencies = [ "x11", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows 0.48.0", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3061,6 +3854,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" +dependencies = [ + "rustix 0.38.35", + "windows-targets 0.52.6", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -3205,8 +4008,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata", + "regex-syntax", ] [[package]] @@ -3220,13 +4023,35 @@ dependencies = [ "system-deps", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -3283,6 +4108,25 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.6" @@ -3294,7 +4138,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.1.0", "indexmap 2.5.0", "slab", "tokio", @@ -3345,6 +4189,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "heck" version = "0.4.1" @@ -3425,6 +4280,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + [[package]] name = "http" version = "1.1.0" @@ -3436,6 +4302,17 @@ dependencies = [ "itoa 1.0.11", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -3443,7 +4320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.1.0", ] [[package]] @@ -3454,8 +4331,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -3477,6 +4354,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.11", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.4.1" @@ -3486,9 +4387,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa 1.0.11", @@ -3498,6 +4399,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.3" @@ -3505,8 +4422,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.1.0", + "hyper 1.4.1", "hyper-util", "rustls 0.23.12", "rustls-pki-types", @@ -3523,7 +4440,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-util", "native-tls", "tokio", @@ -3540,9 +4457,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -3619,30 +4536,12 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.7", + "regex-automata", "same-file", "walkdir", "winapi-util", ] -[[package]] -name = "image" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-traits", - "png", - "qoi", - "tiff", -] - [[package]] name = "image" version = "0.25.2" @@ -3740,13 +4639,19 @@ dependencies = [ [[package]] name = "infer" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199" +checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" dependencies = [ "cfb", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inotify" version = "0.9.6" @@ -3773,6 +4678,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -3822,6 +4728,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "is-wsl" version = "0.4.0" @@ -3832,6 +4749,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3853,6 +4779,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -3953,9 +4888,6 @@ name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" -dependencies = [ - "rayon", -] [[package]] name = "js-sys" @@ -4013,9 +4945,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec465b607a36dc5dd45d48b7689bc83f679f66a3ac6b6b21cc787a11e0f8685" +checksum = "02f01f48e04e0d7da72280ab787c9943695699c9b32b99158ece105e8ad0afea" dependencies = [ "jsonrpsee-core", "jsonrpsee-server", @@ -4025,13 +4957,13 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f0977f9c15694371b8024c35ab58ca043dbbf4b51ccb03db8858a021241df1" +checksum = "d80eccbd47a7b9f1e67663fd846928e941cb49c65236e297dd11c9ea3c5e3387" dependencies = [ "base64 0.22.1", "futures-util", - "http", + "http 1.1.0", "jsonrpsee-core", "pin-project", "soketto", @@ -4044,16 +4976,16 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e942c55635fbf5dc421938b8558a8141c7e773720640f4f1dbe1f4164ca4e221" +checksum = "3c2709a32915d816a6e8f625bf72cf74523ebe5d8829f895d6b041b1d3137818" dependencies = [ "async-trait", "bytes", "futures-timer", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.1", "http-body-util", "jsonrpsee-types", "parking_lot", @@ -4070,15 +5002,15 @@ dependencies = [ [[package]] name = "jsonrpsee-server" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038fb697a709bec7134e9ccbdbecfea0e2d15183f7140254afef7c5610a3f488" +checksum = "e30110d0f2d7866c8cc6c86483bdab2eb9f4d2f0e20db55518b2bca84651ba8e" dependencies = [ "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -4097,11 +5029,11 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b67d6e008164f027afbc2e7bb79662650158d26df200040282d2aa1cbb093b" +checksum = "1ca331cd7b3fe95b33432825c2d4c9f5a43963e207fdc01ae67f9fd80ab0930f" dependencies = [ - "http", + "http 1.1.0", "serde", "serde_json", "thiserror", @@ -4109,11 +5041,11 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "992bf67d1132f88edf4a4f8cff474cf01abb2be203004a2b8e11c2b20795b99e" +checksum = "755ca3da1c67671f1fae01cd1a47f41dfb2233a8f19a643e587ab0a663942044" dependencies = [ - "http", + "http 1.1.0", "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-types", @@ -4142,7 +5074,7 @@ dependencies = [ "parking_lot", "percent-encoding", "regex", - "reqwest", + "reqwest 0.12.7", "serde", "serde_json", "time", @@ -4150,6 +5082,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "jzon" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ab85f84ca42c5ec520e6f3c9966ba1fd62909ce260f8837e248857d2560509" + [[package]] name = "k256" version = "0.13.3" @@ -4157,11 +5110,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", "sha2", - "signature", + "signature 2.2.0", ] [[package]] @@ -4184,6 +5137,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -4359,21 +5332,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", -] - [[package]] name = "loop9" version = "0.1.5" @@ -4383,6 +5341,15 @@ dependencies = [ "imgref", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.0", +] + [[package]] name = "lzma-sys" version = "0.1.20" @@ -4449,15 +5416,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "matches" version = "0.1.10" @@ -4559,6 +5517,26 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minicbor" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d15f4203d71fdf90903c2696e55426ac97a363c67b218488a73b534ce7aca10" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4621,16 +5599,17 @@ dependencies = [ [[package]] name = "muda" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" +checksum = "b8123dfd4996055ac9b15a60ad263b44b01e539007523ad7a4a533a3d93b0591" dependencies = [ - "cocoa 0.26.0", "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc", + "objc2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "png", "serde", @@ -4812,16 +5791,6 @@ dependencies = [ "zbus 4.4.0", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num" version = "0.4.3" @@ -5106,6 +6075,20 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.14.5", + "indexmap 2.5.0", + "memchr", + "ruzstd", +] + [[package]] name = "object" version = "0.36.4" @@ -5127,6 +6110,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once-cell-regex" version = "0.2.1" @@ -5261,10 +6253,10 @@ dependencies = [ ] [[package]] -name = "overload" -version = "0.1.1" +name = "outref" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" [[package]] name = "owo-colors" @@ -5375,14 +6367,42 @@ dependencies = [ "unicode-id-start", ] +[[package]] +name = "p12" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224" +dependencies = [ + "cbc", + "cipher", + "des", + "getrandom 0.2.15", + "hmac", + "lazy_static", + "rc2", + "sha1", + "yasna", +] + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2", +] + [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2", ] @@ -5393,8 +6413,8 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2", ] @@ -5405,9 +6425,9 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", + "base16ct 0.2.0", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "rand_core 0.6.4", "sha2", @@ -5512,6 +6532,29 @@ dependencies = [ "hmac", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.77", +] + [[package]] name = "pem" version = "3.0.4" @@ -5611,9 +6654,9 @@ dependencies = [ "digest", "dsa", "eax", - "ecdsa", + "ecdsa 0.16.9", "ed25519-dalek", - "elliptic-curve", + "elliptic-curve 0.13.8", "flate2", "generic-array", "hex", @@ -5628,7 +6671,7 @@ dependencies = [ "num-traits", "num_enum", "ocb3", - "p256", + "p256 0.13.2", "p384", "p521", "rand 0.8.5", @@ -5638,7 +6681,7 @@ dependencies = [ "sha1-checked", "sha2", "sha3", - "signature", + "signature 2.2.0", "smallvec", "thiserror", "twofish", @@ -5835,9 +6878,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.9", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -5846,8 +6899,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.9", + "spki 0.7.3", ] [[package]] @@ -5856,6 +6909,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.7.0" @@ -5944,13 +7003,13 @@ dependencies = [ "glib-sys", "gtk", "gtk-sys", - "http", + "http 1.1.0", "lazy_static", "log", "notify-rust", "open", - "reqwest", - "rfd", + "reqwest 0.12.7", + "rfd 0.14.1", "rust-ini", "serde", "serde_json", @@ -6004,7 +7063,7 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", ] [[package]] @@ -6074,6 +7133,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "version_check", + "yansi", +] + [[package]] name = "profiling" version = "1.0.15" @@ -6146,6 +7218,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.37" @@ -6242,6 +7323,44 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rasn" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9b0d03fbc7d2dcfdd35086c43ce30ac5ff62ed7eff4397e4f4f2995a2b0e2a" +dependencies = [ + "arrayvec 0.7.6", + "bitvec", + "bitvec-nom2", + "bytes", + "chrono", + "either", + "jzon", + "konst", + "nom", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "rasn-derive", + "snafu", +] + +[[package]] +name = "rasn-derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbaf7105cd254b632f4732fbcc243ce750cef87d8335826125ef6df5733b5a0c" +dependencies = [ + "either", + "itertools 0.10.5", + "proc-macro2", + "quote", + "rayon", + "syn 1.0.109", + "uuid", +] + [[package]] name = "rav1e" version = "0.7.1" @@ -6317,6 +7436,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -6362,17 +7490,8 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -6383,14 +7502,14 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.29" +name = "regex-lite" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" @@ -6407,6 +7526,48 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg 0.50.0", +] + [[package]] name = "reqwest" version = "0.12.7" @@ -6419,12 +7580,12 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.4.1", + "hyper-rustls 0.27.3", "hyper-tls", "hyper-util", "ipnet", @@ -6435,12 +7596,12 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 2.1.3", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", - "system-configuration", + "system-configuration 0.6.1", "tokio", "tokio-native-tls", "tokio-util", @@ -6470,6 +7631,17 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -6504,6 +7676,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rfd" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e" +dependencies = [ + "ashpd 0.9.1", + "block2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rgb" version = "0.8.50" @@ -6631,10 +7826,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -6708,6 +7903,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.37.27" @@ -6735,15 +7939,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ + "log", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.7", "subtle", "zeroize", ] @@ -6758,11 +7975,45 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.7", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pemfile" version = "2.1.3" @@ -6779,6 +8030,16 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.102.7" @@ -6814,6 +8075,17 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ruzstd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" +dependencies = [ + "byteorder", + "derive_more", + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.18" @@ -6859,6 +8131,7 @@ dependencies = [ "serde", "serde_json", "url", + "uuid", ] [[package]] @@ -6885,6 +8158,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -6896,22 +8189,46 @@ dependencies = [ "sha2", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.9", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -7004,6 +8321,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.209" @@ -7120,6 +8449,19 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.5.0", + "itoa 1.0.11", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.1" @@ -7209,15 +8551,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shared_child" version = "1.0.1" @@ -7249,6 +8582,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -7280,6 +8623,24 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simple-file-manifest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd19be0257552dd56d1bb6946f89f193c6e5b9f13cc9327c4bc84a357507c74" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "simplecss" version = "0.2.1" @@ -7331,6 +8692,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.4.10" @@ -7394,7 +8778,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http", + "http 1.1.0", "httparse", "log", "rand 0.8.5", @@ -7427,6 +8811,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spake2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5482afe85a0b6ce956c945401598dbc527593c77ba51d0a87a586938b1b893a" +dependencies = [ + "curve25519-dalek", + "hkdf", + "rand_core 0.6.4", + "sha2", +] + [[package]] name = "spin" version = "0.9.8" @@ -7436,6 +8832,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -7443,7 +8849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -7452,15 +8858,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "state" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" -dependencies = [ - "loom", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -7692,6 +9089,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "sys-locale" version = "0.3.1" @@ -7714,6 +9123,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -7722,7 +9142,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -7750,12 +9180,12 @@ dependencies = [ [[package]] name = "tao" -version = "0.30.0" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e" +checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751" dependencies = [ "bitflags 2.6.0", - "cocoa 0.26.0", + "cocoa", "core-foundation 0.10.0", "core-graphics 0.24.0", "crossbeam-channel", @@ -7823,13 +9253,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.0-rc.9" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b805e6bf5f6a4df7d1a64b2952d33fca6d538746efe9c9cdae4157a1efc5b17" +checksum = "5920aad0804ea5e86808d4b6e8753d3bcbae7efc8f4e41a4da00b45427559868" dependencies = [ "anyhow", "bytes", - "cocoa 0.26.0", "dirs 5.0.1", "dunce", "embed_plist", @@ -7838,28 +9267,30 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http", - "image 0.24.9", + "http 1.1.0", + "image", "jni", "libc", "log", "mime", "muda", - "objc", + "objc2", + "objc2-app-kit", + "objc2-foundation", "percent-encoding", + "plist", "raw-window-handle", - "reqwest", + "reqwest 0.12.7", "serde", "serde_json", "serde_repr", "serialize-to-javascript", - "state", "swift-rs", "tauri-build", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "thiserror", "tokio", "tray-icon", @@ -7873,9 +9304,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.0-rc.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4acec578ff9de14da177722c8fb5e3d6c88af296696190c70b83bec91437248a" +checksum = "935f9b3c49b22b3e2e485a57f46d61cd1ae07b1cbb2ba87387a387caf2d8c4e7" dependencies = [ "anyhow", "cargo_toml", @@ -7887,7 +9318,7 @@ dependencies = [ "semver", "serde", "serde_json", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "tauri-winres", "toml 0.8.19", "walkdir", @@ -7895,9 +9326,9 @@ dependencies = [ [[package]] name = "tauri-bundler" -version = "2.0.1-rc.8" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850672a9b3316d0c3abea71891d07184c6e5176b68e7c6fb0b4d5432c08f7e43" +checksum = "cc59028d72f54cd39aecd61c662fc0a879473962bedb3de98eb129f59ccb0207" dependencies = [ "anyhow", "ar", @@ -7909,7 +9340,7 @@ dependencies = [ "handlebars", "heck 0.5.0", "hex", - "image 0.25.2", + "image", "log", "md5", "os_pipe", @@ -7925,23 +9356,24 @@ dependencies = [ "tar", "tauri-icns", "tauri-macos-sign", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "tempfile", "thiserror", "time", "ureq", + "url", "uuid", "walkdir", "windows-registry", "windows-sys 0.59.0", - "zip", + "zip 2.2.0", ] [[package]] name = "tauri-cli" -version = "2.0.0-rc.10" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6581f7a8e460dd8b2d9359728a90a034e7ccb0eed6d42cfb1da43f8c4842077d" +checksum = "1c793c2f9430eb95040abac4ac78cd5a955c7aae13213a7d54e3e209c161c369" dependencies = [ "anyhow", "ar", @@ -7958,13 +9390,13 @@ dependencies = [ "duct", "dunce", "elf", - "env_logger", + "env_logger 0.11.5", "glob", "handlebars", "heck 0.5.0", "html5ever", "ignore", - "image 0.25.2", + "image", "include_dir", "itertools 0.13.0", "json-patch 2.0.0", @@ -7982,7 +9414,7 @@ dependencies = [ "minisign", "notify", "notify-debouncer-mini", - "object", + "object 0.36.4", "os_info", "os_pipe", "oxc_allocator", @@ -7991,6 +9423,7 @@ dependencies = [ "oxc_span", "phf 0.11.2", "plist", + "rand 0.8.5", "regex", "resvg", "semver", @@ -8003,22 +9436,23 @@ dependencies = [ "tauri-icns", "tauri-macos-sign", "tauri-utils 1.6.0", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "tempfile", "tokio", "toml 0.8.19", "toml_edit 0.22.20", "ureq", "url", + "uuid", "walkdir", "windows-sys 0.59.0", ] [[package]] name = "tauri-codegen" -version = "2.0.0-rc.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0744bec087358e5de9a078a1b19346ed9b775f578395975f5a74ccd0c717b22a" +checksum = "95d7443dd4f0b597704b6a14b964ee2ed16e99928d8e6292ae9825f09fbcd30e" dependencies = [ "base64 0.22.1", "brotli", @@ -8033,7 +9467,7 @@ dependencies = [ "serde_json", "sha2", "syn 2.0.77", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "thiserror", "time", "url", @@ -8053,15 +9487,18 @@ dependencies = [ [[package]] name = "tauri-macos-sign" -version = "0.1.1-rc.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea6debc5d6182af338fb92bda2b399332d6495d3cbc1238e5cbbf82ad56c09b" +checksum = "be95e2d37d1e3605831da071234418c786684af48982509af1b7f50b31af4546" dependencies = [ "anyhow", + "apple-codesign", + "chrono", "dirs-next", "log", "once-cell-regex", "os_pipe", + "p12", "plist", "rand 0.8.5", "serde", @@ -8072,23 +9509,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-rc.7" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b043cac341130f288044dca76fae8e62d7c18fdcd8012239a66af03868b7ca37" +checksum = "4d2c0963ccfc3f5194415f2cce7acc975942a8797fbabfb0aa1ed6f59326ae7f" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.77", "tauri-codegen", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", ] [[package]] name = "tauri-plugin" -version = "2.0.0-rc.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9bb31aad7296f85df545171023c72a263b54aac350197f923893fb5e6f90b4" +checksum = "b2e6660a409963e4d57b9bfab4addd141eeff41bd3a7fb14e13004a832cf7ef6" dependencies = [ "anyhow", "glob", @@ -8096,19 +9533,19 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "toml 0.8.19", "walkdir", ] [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.0.0-rc.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9d78deb78704d9eeedc23892fc6b25a944c7f9c59354a0ea9a4fb5ab043794" +checksum = "78b7d556886c15849198c0948fd7f4c880492f0461539176da0a8a70272e2904" dependencies = [ "arboard", - "image 0.24.9", + "image", "log", "serde", "serde_json", @@ -8119,14 +9556,13 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.0.0-rc.4" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191a81b01083881a388879e269eec60b582eed6fc3e16884dc394ff1791b0c19" +checksum = "ddb2fe88b602461c118722c574e2775ab26a4e68886680583874b2f6520608b7" dependencies = [ - "dunce", "log", "raw-window-handle", - "rfd", + "rfd 0.15.0", "serde", "serde_json", "tauri", @@ -8138,12 +9574,14 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.0.0-rc.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0704205734f3b4f37d7bf9e9072537716371741bf98b22a22cc5e5340e512467" +checksum = "ab300488ebec3487ca5f56289692e7e45feb07eea8d5e1dba497f7dc9dd9c407" dependencies = [ "anyhow", + "dunce", "glob", + "percent-encoding", "schemars", "serde", "serde_json", @@ -8157,13 +9595,13 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.0.0-rc.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b57e4666c4a5d81f81b7bb8eacf51ae32c4e69c35071aabb480ad20a80836e4e" +checksum = "9a49f2c05d15e6375ab7f7e528b3049150ba4dfafdf61f85e5178d0aef18e3f5" dependencies = [ "android_logger", "byte-unit", - "cocoa 0.25.0", + "cocoa", "fern", "log", "objc", @@ -8179,9 +9617,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.0.0-rc.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf32fba3a2650f89c7dfc532b38de566a37779e482f07dccabf45fe24444e26" +checksum = "ef492a2d19b6376bb4c9e0c4fab3f3bf8a220ea112d24f35027b737ff55de20c" dependencies = [ "log", "notify-rust", @@ -8198,11 +9636,11 @@ dependencies = [ [[package]] name = "tauri-plugin-os" -version = "2.0.0-rc.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee761edd532fce2232453e9c8e0f7d9c0b6fe125c4b90b3eb4362ee84224" +checksum = "fbc5f23a86f37687c7f4fecfdc706b279087bc44f7a46702f7307ff1551ee03a" dependencies = [ - "gethostname", + "gethostname 0.5.0", "log", "os_info", "serde", @@ -8216,9 +9654,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.0-rc.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83800ddf78b820172efb5ed7310344e8e4f97fd30cd8237a3f20c12a79eb136" +checksum = "371fb9aca2823990a2d0db7970573be5fdf07881fcaa2b835b29631feb84aec1" dependencies = [ "encoding_rs", "log", @@ -8237,9 +9675,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.0.0-rc.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73c92c98d44d4daba0118d905f45243dfcd6eaac82216c3382a02d17cb74cf2" +checksum = "a25ac834491d089699a2bc9266a662faf373c9f779f05a2235bc6e4d9e61769a" dependencies = [ "log", "serde", @@ -8252,9 +9690,9 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" -version = "2.0.0-rc.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303569dd7858361d4c623845448b136b4c95d53b5d2bde6630bea9d7f0022d45" +checksum = "fd1cef203a15b4772898e7bc8e57c1f34696e39848987dfcd294d51ba0525650" dependencies = [ "bitflags 2.6.0", "log", @@ -8267,18 +9705,18 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.0.0-rc.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c7a6530acc06640e8f07cfeb01ac694f1de2f4e565525a2199e0dca80ff9f7" +checksum = "af12ad1af974b274ef1d32a94e6eba27a312b429ef28fcb98abc710df7f9151d" dependencies = [ "dpi", "gtk", - "http", + "http 1.1.0", "jni", "raw-window-handle", "serde", "serde_json", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "thiserror", "url", "windows 0.58.0", @@ -8286,21 +9724,23 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.0.0-rc.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fcadbc24646c8d3362ed4e332cb42932e08c632220a20a61cb7e5fe36ddd85c" +checksum = "e45e88aa0b11b302d836e6ea3e507a6359044c4a8bc86b865ba99868c695753d" dependencies = [ - "cocoa 0.26.0", "gtk", - "http", + "http 1.1.0", "jni", "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", "percent-encoding", "raw-window-handle", "softbuffer", "tao", "tauri-runtime", - "tauri-utils 2.0.0-rc.8", + "tauri-utils 2.0.1", "url", "webkit2gtk", "webview2-com", @@ -8341,9 +9781,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.0-rc.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201498c8281ab2597e344b4a4c923e8d491782305979d71e7cdf8fb79aab5948" +checksum = "c38b0230d6880cf6dd07b6d7dd7789a0869f98ac12146e0d18d1c1049215a045" dependencies = [ "aes-gcm", "brotli", @@ -8353,7 +9793,7 @@ dependencies = [ "getrandom 0.2.15", "glob", "html5ever", - "infer 0.15.0", + "infer 0.16.0", "json-patch 2.0.0", "json5", "kuchikiki", @@ -8375,6 +9815,7 @@ dependencies = [ "toml 0.8.19", "url", "urlpattern", + "uuid", "walkdir", ] @@ -8434,6 +9875,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.2.6" @@ -8491,16 +9941,6 @@ dependencies = [ "syn 2.0.77", ] -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tiff" version = "0.9.1" @@ -8634,6 +10074,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -8704,7 +10154,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "http", + "http 1.1.0", "httparse", "rand 0.8.5", "ring", @@ -8843,43 +10293,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] name = "tray-icon" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "131a65b2cef2081bc14dbcd414c906edbfa3bb5323dd7e748cc298614681196b" +checksum = "533fc2d4105e0e3d96ce1c71f2d308c9fbbe2ef9c587cab63dd627ab5bde218f" dependencies = [ "core-graphics 0.24.0", "crossbeam-channel", @@ -8920,10 +10340,13 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand 0.8.5", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pki-types", "sha1", "thiserror", "url", @@ -8939,6 +10362,16 @@ dependencies = [ "cipher", ] +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "typeid" version = "1.0.2" @@ -8951,6 +10384,21 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "ucd-trie" version = "0.1.6" @@ -8977,6 +10425,15 @@ dependencies = [ "libc", ] +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -9093,6 +10550,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -9103,6 +10566,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -9123,7 +10592,7 @@ dependencies = [ "rustls-pki-types", "socks", "url", - "webpki-roots", + "webpki-roots 0.26.5", ] [[package]] @@ -9139,12 +10608,17 @@ dependencies = [ ] [[package]] -name = "urlpattern" -version = "0.2.0" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" dependencies = [ - "derive_more", "regex", "serde", "unic-ucd-ident", @@ -9203,6 +10677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", + "serde", "sha1_smol", ] @@ -9217,12 +10692,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "value-bag" version = "1.9.0" @@ -9289,6 +10758,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "vswhom" version = "0.1.0" @@ -9426,6 +10901,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.35", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" +dependencies = [ + "bitflags 2.6.0", + "rustix 0.38.35", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +dependencies = [ + "proc-macro2", + "quick-xml 0.36.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.70" @@ -9480,6 +11015,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.5" @@ -9586,22 +11127,13 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8cdd6999298d969289d8078dae02ce798ad23452075985cccba8b6326711ecf" dependencies = [ - "cocoa 0.26.0", + "cocoa", "objc", "raw-window-handle", "windows-sys 0.59.0", "windows-version", ] -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows" version = "0.54.0" @@ -10011,6 +11543,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.52.0" @@ -10029,13 +11571,13 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wry" -version = "0.43.1" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4d715cf5fe88e9647f3d17b207b6d060d4a88e7171d4ccb2d2c657dd1d44728" +checksum = "440600584cfbd8b0d28eace95c1f2c253db05dae43780b79380aa1e868f04c73" dependencies = [ "base64 0.22.1", "block", - "cocoa 0.26.0", + "cocoa", "core-graphics 0.24.0", "crossbeam-channel", "dpi", @@ -10043,7 +11585,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http", + "http 1.1.0", "javascriptcore-rs", "jni", "kuchikiki", @@ -10103,7 +11645,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ - "gethostname", + "gethostname 0.4.3", "rustix 0.38.35", "x11rb-protocol", ] @@ -10126,6 +11668,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3cec94c3999f31341553f358ef55f65fc031291a022cd42ec0ce7219560c76" +dependencies = [ + "chrono", + "cookie-factory", +] + [[package]] name = "x509-certificate" version = "0.23.1" @@ -10135,12 +11687,12 @@ dependencies = [ "bcder", "bytes", "chrono", - "der", + "der 0.7.9", "hex", "pem", "ring", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "thiserror", "zeroize", ] @@ -10172,6 +11724,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xmlwriter" version = "0.1.0" @@ -10187,6 +11751,18 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" + [[package]] name = "zbus" version = "3.15.2" @@ -10357,6 +11933,18 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zip" version = "2.2.0" @@ -10374,6 +11962,17 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zip_structs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce824a6bfffe8942820fa36d24973b7c83a40896749a42e33de0abdd11750ee5" +dependencies = [ + "byteorder", + "bytesize", + "thiserror", +] + [[package]] name = "zopfli" version = "0.8.1" diff --git a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json index 233ccc01..1d974eb0 100644 --- a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json +++ b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json index 797ccb5c..10fb08fb 100644 --- a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json @@ -133,2803 +133,2202 @@ { "description": "Reference a permission or permission set by identifier and extends its scope.", "type": "object", - "oneOf": [ + "allOf": [ { - "type": "object", - "required": [ - "identifier" - ], + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", + "type": "string", + "const": "shell:default" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute" + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill" + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open" + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn" + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write" + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute" + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill" + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open" + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn" + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write" + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, "properties": { "identifier": { - "oneOf": [ + "description": "Identifier of the permission or permission set.", + "allOf": [ { - "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", - "type": "string", - "enum": [ - "shell:default" - ] - }, + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ { - "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-execute" - ] - }, - { - "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-kill" - ] - }, - { - "description": "shell:allow-open -> Enables the open command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-open" - ] - }, - { - "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-spawn" - ] - }, - { - "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-stdin-write" - ] - }, - { - "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-execute" - ] - }, - { - "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-kill" - ] - }, - { - "description": "shell:deny-open -> Denies the open command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-open" - ] - }, - { - "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-spawn" - ] - }, - { - "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-stdin-write" - ] + "$ref": "#/definitions/Identifier" } ] }, "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], "items": { - "title": "Entry", - "description": "A command allowed to be executed by the webview API.", - "type": "object", - "required": [ - "args", - "cmd", - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - } + "$ref": "#/definitions/Value" } }, "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], "items": { - "title": "Entry", - "description": "A command allowed to be executed by the webview API.", - "type": "object", - "required": [ - "args", - "cmd", - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - } + "$ref": "#/definitions/Value" } } } } + ], + "required": [ + "identifier" ] } ] }, "Identifier": { + "description": "Permission identifier", "oneOf": [ { - "description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n", + "description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n", "type": "string", - "enum": [ - "clipboard-manager:default" - ] + "const": "clipboard-manager:default" }, { - "description": "clipboard-manager:allow-clear -> Enables the clear command without any pre-configured scope.", + "description": "Enables the clear command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-clear" - ] + "const": "clipboard-manager:allow-clear" }, { - "description": "clipboard-manager:allow-read-image -> Enables the read_image command without any pre-configured scope.", + "description": "Enables the read_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-read-image" - ] + "const": "clipboard-manager:allow-read-image" }, { - "description": "clipboard-manager:allow-read-text -> Enables the read_text command without any pre-configured scope.", + "description": "Enables the read_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-read-text" - ] + "const": "clipboard-manager:allow-read-text" }, { - "description": "clipboard-manager:allow-write-html -> Enables the write_html command without any pre-configured scope.", + "description": "Enables the write_html command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-write-html" - ] + "const": "clipboard-manager:allow-write-html" }, { - "description": "clipboard-manager:allow-write-image -> Enables the write_image command without any pre-configured scope.", + "description": "Enables the write_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-write-image" - ] + "const": "clipboard-manager:allow-write-image" }, { - "description": "clipboard-manager:allow-write-text -> Enables the write_text command without any pre-configured scope.", + "description": "Enables the write_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-write-text" - ] + "const": "clipboard-manager:allow-write-text" }, { - "description": "clipboard-manager:deny-clear -> Denies the clear command without any pre-configured scope.", + "description": "Denies the clear command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-clear" - ] + "const": "clipboard-manager:deny-clear" }, { - "description": "clipboard-manager:deny-read-image -> Denies the read_image command without any pre-configured scope.", + "description": "Denies the read_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-read-image" - ] + "const": "clipboard-manager:deny-read-image" }, { - "description": "clipboard-manager:deny-read-text -> Denies the read_text command without any pre-configured scope.", + "description": "Denies the read_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-read-text" - ] + "const": "clipboard-manager:deny-read-text" }, { - "description": "clipboard-manager:deny-write-html -> Denies the write_html command without any pre-configured scope.", + "description": "Denies the write_html command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-write-html" - ] + "const": "clipboard-manager:deny-write-html" }, { - "description": "clipboard-manager:deny-write-image -> Denies the write_image command without any pre-configured scope.", + "description": "Denies the write_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-write-image" - ] + "const": "clipboard-manager:deny-write-image" }, { - "description": "clipboard-manager:deny-write-text -> Denies the write_text command without any pre-configured scope.", + "description": "Denies the write_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-write-text" - ] + "const": "clipboard-manager:deny-write-text" }, { - "description": "core:app:default -> Default permissions for the plugin.", + "description": "Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n", "type": "string", - "enum": [ - "core:app:default" - ] + "const": "core:default" }, { - "description": "core:app:allow-app-hide -> Enables the app_hide command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:app:allow-app-hide" - ] + "const": "core:app:default" }, { - "description": "core:app:allow-app-show -> Enables the app_show command without any pre-configured scope.", + "description": "Enables the app_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-app-show" - ] + "const": "core:app:allow-app-hide" }, { - "description": "core:app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.", + "description": "Enables the app_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-default-window-icon" - ] + "const": "core:app:allow-app-show" }, { - "description": "core:app:allow-name -> Enables the name command without any pre-configured scope.", + "description": "Enables the default_window_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-name" - ] + "const": "core:app:allow-default-window-icon" }, { - "description": "core:app:allow-tauri-version -> Enables the tauri_version command without any pre-configured scope.", + "description": "Enables the name command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-tauri-version" - ] + "const": "core:app:allow-name" }, { - "description": "core:app:allow-version -> Enables the version command without any pre-configured scope.", + "description": "Enables the set_app_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-version" - ] + "const": "core:app:allow-set-app-theme" }, { - "description": "core:app:deny-app-hide -> Denies the app_hide command without any pre-configured scope.", + "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-app-hide" - ] + "const": "core:app:allow-tauri-version" }, { - "description": "core:app:deny-app-show -> Denies the app_show command without any pre-configured scope.", + "description": "Enables the version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-app-show" - ] + "const": "core:app:allow-version" }, { - "description": "core:app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.", + "description": "Denies the app_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-default-window-icon" - ] + "const": "core:app:deny-app-hide" }, { - "description": "core:app:deny-name -> Denies the name command without any pre-configured scope.", + "description": "Denies the app_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-name" - ] + "const": "core:app:deny-app-show" }, { - "description": "core:app:deny-tauri-version -> Denies the tauri_version command without any pre-configured scope.", + "description": "Denies the default_window_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-tauri-version" - ] + "const": "core:app:deny-default-window-icon" }, { - "description": "core:app:deny-version -> Denies the version command without any pre-configured scope.", + "description": "Denies the name command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-version" - ] + "const": "core:app:deny-name" }, { - "description": "core:event:default -> Default permissions for the plugin.", + "description": "Denies the set_app_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:default" - ] + "const": "core:app:deny-set-app-theme" }, { - "description": "core:event:allow-emit -> Enables the emit command without any pre-configured scope.", + "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:allow-emit" - ] + "const": "core:app:deny-tauri-version" }, { - "description": "core:event:allow-emit-to -> Enables the emit_to command without any pre-configured scope.", + "description": "Denies the version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:allow-emit-to" - ] + "const": "core:app:deny-version" }, { - "description": "core:event:allow-listen -> Enables the listen command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:event:allow-listen" - ] + "const": "core:event:default" }, { - "description": "core:event:allow-unlisten -> Enables the unlisten command without any pre-configured scope.", + "description": "Enables the emit command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:allow-unlisten" - ] + "const": "core:event:allow-emit" }, { - "description": "core:event:deny-emit -> Denies the emit command without any pre-configured scope.", + "description": "Enables the emit_to command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-emit" - ] + "const": "core:event:allow-emit-to" }, { - "description": "core:event:deny-emit-to -> Denies the emit_to command without any pre-configured scope.", + "description": "Enables the listen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-emit-to" - ] + "const": "core:event:allow-listen" }, { - "description": "core:event:deny-listen -> Denies the listen command without any pre-configured scope.", + "description": "Enables the unlisten command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-listen" - ] + "const": "core:event:allow-unlisten" }, { - "description": "core:event:deny-unlisten -> Denies the unlisten command without any pre-configured scope.", + "description": "Denies the emit command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-unlisten" - ] + "const": "core:event:deny-emit" }, { - "description": "core:image:default -> Default permissions for the plugin.", + "description": "Denies the emit_to command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:default" - ] + "const": "core:event:deny-emit-to" }, { - "description": "core:image:allow-from-bytes -> Enables the from_bytes command without any pre-configured scope.", + "description": "Denies the listen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-from-bytes" - ] + "const": "core:event:deny-listen" }, { - "description": "core:image:allow-from-path -> Enables the from_path command without any pre-configured scope.", + "description": "Denies the unlisten command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-from-path" - ] + "const": "core:event:deny-unlisten" }, { - "description": "core:image:allow-new -> Enables the new command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:image:allow-new" - ] + "const": "core:image:default" }, { - "description": "core:image:allow-rgba -> Enables the rgba command without any pre-configured scope.", + "description": "Enables the from_bytes command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-rgba" - ] + "const": "core:image:allow-from-bytes" }, { - "description": "core:image:allow-size -> Enables the size command without any pre-configured scope.", + "description": "Enables the from_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-size" - ] + "const": "core:image:allow-from-path" }, { - "description": "core:image:deny-from-bytes -> Denies the from_bytes command without any pre-configured scope.", + "description": "Enables the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-from-bytes" - ] + "const": "core:image:allow-new" }, { - "description": "core:image:deny-from-path -> Denies the from_path command without any pre-configured scope.", + "description": "Enables the rgba command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-from-path" - ] + "const": "core:image:allow-rgba" }, { - "description": "core:image:deny-new -> Denies the new command without any pre-configured scope.", + "description": "Enables the size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-new" - ] + "const": "core:image:allow-size" }, { - "description": "core:image:deny-rgba -> Denies the rgba command without any pre-configured scope.", + "description": "Denies the from_bytes command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-rgba" - ] + "const": "core:image:deny-from-bytes" }, { - "description": "core:image:deny-size -> Denies the size command without any pre-configured scope.", + "description": "Denies the from_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-size" - ] + "const": "core:image:deny-from-path" }, { - "description": "core:menu:default -> Default permissions for the plugin.", + "description": "Denies the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:default" - ] + "const": "core:image:deny-new" }, { - "description": "core:menu:allow-append -> Enables the append command without any pre-configured scope.", + "description": "Denies the rgba command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-append" - ] + "const": "core:image:deny-rgba" }, { - "description": "core:menu:allow-create-default -> Enables the create_default command without any pre-configured scope.", + "description": "Denies the size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-create-default" - ] + "const": "core:image:deny-size" }, { - "description": "core:menu:allow-get -> Enables the get command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:menu:allow-get" - ] + "const": "core:menu:default" }, { - "description": "core:menu:allow-insert -> Enables the insert command without any pre-configured scope.", + "description": "Enables the append command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-insert" - ] + "const": "core:menu:allow-append" }, { - "description": "core:menu:allow-is-checked -> Enables the is_checked command without any pre-configured scope.", + "description": "Enables the create_default command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-is-checked" - ] + "const": "core:menu:allow-create-default" }, { - "description": "core:menu:allow-is-enabled -> Enables the is_enabled command without any pre-configured scope.", + "description": "Enables the get command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-is-enabled" - ] + "const": "core:menu:allow-get" }, { - "description": "core:menu:allow-items -> Enables the items command without any pre-configured scope.", + "description": "Enables the insert command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-items" - ] + "const": "core:menu:allow-insert" }, { - "description": "core:menu:allow-new -> Enables the new command without any pre-configured scope.", + "description": "Enables the is_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-new" - ] + "const": "core:menu:allow-is-checked" }, { - "description": "core:menu:allow-popup -> Enables the popup command without any pre-configured scope.", + "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-popup" - ] + "const": "core:menu:allow-is-enabled" }, { - "description": "core:menu:allow-prepend -> Enables the prepend command without any pre-configured scope.", + "description": "Enables the items command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-prepend" - ] + "const": "core:menu:allow-items" }, { - "description": "core:menu:allow-remove -> Enables the remove command without any pre-configured scope.", + "description": "Enables the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-remove" - ] + "const": "core:menu:allow-new" }, { - "description": "core:menu:allow-remove-at -> Enables the remove_at command without any pre-configured scope.", + "description": "Enables the popup command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-remove-at" - ] + "const": "core:menu:allow-popup" }, { - "description": "core:menu:allow-set-accelerator -> Enables the set_accelerator command without any pre-configured scope.", + "description": "Enables the prepend command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-accelerator" - ] + "const": "core:menu:allow-prepend" }, { - "description": "core:menu:allow-set-as-app-menu -> Enables the set_as_app_menu command without any pre-configured scope.", + "description": "Enables the remove command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-app-menu" - ] + "const": "core:menu:allow-remove" }, { - "description": "core:menu:allow-set-as-help-menu-for-nsapp -> Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "description": "Enables the remove_at command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-help-menu-for-nsapp" - ] + "const": "core:menu:allow-remove-at" }, { - "description": "core:menu:allow-set-as-window-menu -> Enables the set_as_window_menu command without any pre-configured scope.", + "description": "Enables the set_accelerator command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-window-menu" - ] + "const": "core:menu:allow-set-accelerator" }, { - "description": "core:menu:allow-set-as-windows-menu-for-nsapp -> Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "description": "Enables the set_as_app_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-windows-menu-for-nsapp" - ] + "const": "core:menu:allow-set-as-app-menu" }, { - "description": "core:menu:allow-set-checked -> Enables the set_checked command without any pre-configured scope.", + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-checked" - ] + "const": "core:menu:allow-set-as-help-menu-for-nsapp" }, { - "description": "core:menu:allow-set-enabled -> Enables the set_enabled command without any pre-configured scope.", + "description": "Enables the set_as_window_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-enabled" - ] + "const": "core:menu:allow-set-as-window-menu" }, { - "description": "core:menu:allow-set-icon -> Enables the set_icon command without any pre-configured scope.", + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-icon" - ] + "const": "core:menu:allow-set-as-windows-menu-for-nsapp" }, { - "description": "core:menu:allow-set-text -> Enables the set_text command without any pre-configured scope.", + "description": "Enables the set_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-text" - ] + "const": "core:menu:allow-set-checked" }, { - "description": "core:menu:allow-text -> Enables the text command without any pre-configured scope.", + "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-text" - ] + "const": "core:menu:allow-set-enabled" }, { - "description": "core:menu:deny-append -> Denies the append command without any pre-configured scope.", + "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-append" - ] + "const": "core:menu:allow-set-icon" }, { - "description": "core:menu:deny-create-default -> Denies the create_default command without any pre-configured scope.", + "description": "Enables the set_text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-create-default" - ] + "const": "core:menu:allow-set-text" }, { - "description": "core:menu:deny-get -> Denies the get command without any pre-configured scope.", + "description": "Enables the text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-get" - ] + "const": "core:menu:allow-text" }, { - "description": "core:menu:deny-insert -> Denies the insert command without any pre-configured scope.", + "description": "Denies the append command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-insert" - ] + "const": "core:menu:deny-append" }, { - "description": "core:menu:deny-is-checked -> Denies the is_checked command without any pre-configured scope.", + "description": "Denies the create_default command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-is-checked" - ] + "const": "core:menu:deny-create-default" }, { - "description": "core:menu:deny-is-enabled -> Denies the is_enabled command without any pre-configured scope.", + "description": "Denies the get command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-is-enabled" - ] + "const": "core:menu:deny-get" }, { - "description": "core:menu:deny-items -> Denies the items command without any pre-configured scope.", + "description": "Denies the insert command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-items" - ] + "const": "core:menu:deny-insert" }, { - "description": "core:menu:deny-new -> Denies the new command without any pre-configured scope.", + "description": "Denies the is_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-new" - ] + "const": "core:menu:deny-is-checked" }, { - "description": "core:menu:deny-popup -> Denies the popup command without any pre-configured scope.", + "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-popup" - ] + "const": "core:menu:deny-is-enabled" }, { - "description": "core:menu:deny-prepend -> Denies the prepend command without any pre-configured scope.", + "description": "Denies the items command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-prepend" - ] + "const": "core:menu:deny-items" }, { - "description": "core:menu:deny-remove -> Denies the remove command without any pre-configured scope.", + "description": "Denies the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-remove" - ] + "const": "core:menu:deny-new" }, { - "description": "core:menu:deny-remove-at -> Denies the remove_at command without any pre-configured scope.", + "description": "Denies the popup command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-remove-at" - ] + "const": "core:menu:deny-popup" }, { - "description": "core:menu:deny-set-accelerator -> Denies the set_accelerator command without any pre-configured scope.", + "description": "Denies the prepend command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-accelerator" - ] + "const": "core:menu:deny-prepend" }, { - "description": "core:menu:deny-set-as-app-menu -> Denies the set_as_app_menu command without any pre-configured scope.", + "description": "Denies the remove command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-app-menu" - ] + "const": "core:menu:deny-remove" }, { - "description": "core:menu:deny-set-as-help-menu-for-nsapp -> Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "description": "Denies the remove_at command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-help-menu-for-nsapp" - ] + "const": "core:menu:deny-remove-at" }, { - "description": "core:menu:deny-set-as-window-menu -> Denies the set_as_window_menu command without any pre-configured scope.", + "description": "Denies the set_accelerator command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-window-menu" - ] + "const": "core:menu:deny-set-accelerator" }, { - "description": "core:menu:deny-set-as-windows-menu-for-nsapp -> Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "description": "Denies the set_as_app_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-windows-menu-for-nsapp" - ] + "const": "core:menu:deny-set-as-app-menu" }, { - "description": "core:menu:deny-set-checked -> Denies the set_checked command without any pre-configured scope.", + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-checked" - ] + "const": "core:menu:deny-set-as-help-menu-for-nsapp" }, { - "description": "core:menu:deny-set-enabled -> Denies the set_enabled command without any pre-configured scope.", + "description": "Denies the set_as_window_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-enabled" - ] + "const": "core:menu:deny-set-as-window-menu" }, { - "description": "core:menu:deny-set-icon -> Denies the set_icon command without any pre-configured scope.", + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-icon" - ] + "const": "core:menu:deny-set-as-windows-menu-for-nsapp" }, { - "description": "core:menu:deny-set-text -> Denies the set_text command without any pre-configured scope.", + "description": "Denies the set_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-text" - ] + "const": "core:menu:deny-set-checked" }, { - "description": "core:menu:deny-text -> Denies the text command without any pre-configured scope.", + "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-text" - ] + "const": "core:menu:deny-set-enabled" }, { - "description": "core:path:default -> Default permissions for the plugin.", + "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:default" - ] + "const": "core:menu:deny-set-icon" }, { - "description": "core:path:allow-basename -> Enables the basename command without any pre-configured scope.", + "description": "Denies the set_text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-basename" - ] + "const": "core:menu:deny-set-text" }, { - "description": "core:path:allow-dirname -> Enables the dirname command without any pre-configured scope.", + "description": "Denies the text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-dirname" - ] + "const": "core:menu:deny-text" }, { - "description": "core:path:allow-extname -> Enables the extname command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:path:allow-extname" - ] + "const": "core:path:default" }, { - "description": "core:path:allow-is-absolute -> Enables the is_absolute command without any pre-configured scope.", + "description": "Enables the basename command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-is-absolute" - ] + "const": "core:path:allow-basename" }, { - "description": "core:path:allow-join -> Enables the join command without any pre-configured scope.", + "description": "Enables the dirname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-join" - ] + "const": "core:path:allow-dirname" }, { - "description": "core:path:allow-normalize -> Enables the normalize command without any pre-configured scope.", + "description": "Enables the extname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-normalize" - ] + "const": "core:path:allow-extname" }, { - "description": "core:path:allow-resolve -> Enables the resolve command without any pre-configured scope.", + "description": "Enables the is_absolute command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-resolve" - ] + "const": "core:path:allow-is-absolute" }, { - "description": "core:path:allow-resolve-directory -> Enables the resolve_directory command without any pre-configured scope.", + "description": "Enables the join command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-resolve-directory" - ] + "const": "core:path:allow-join" }, { - "description": "core:path:deny-basename -> Denies the basename command without any pre-configured scope.", + "description": "Enables the normalize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-basename" - ] + "const": "core:path:allow-normalize" }, { - "description": "core:path:deny-dirname -> Denies the dirname command without any pre-configured scope.", + "description": "Enables the resolve command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-dirname" - ] + "const": "core:path:allow-resolve" }, { - "description": "core:path:deny-extname -> Denies the extname command without any pre-configured scope.", + "description": "Enables the resolve_directory command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-extname" - ] + "const": "core:path:allow-resolve-directory" }, { - "description": "core:path:deny-is-absolute -> Denies the is_absolute command without any pre-configured scope.", + "description": "Denies the basename command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-is-absolute" - ] + "const": "core:path:deny-basename" }, { - "description": "core:path:deny-join -> Denies the join command without any pre-configured scope.", + "description": "Denies the dirname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-join" - ] + "const": "core:path:deny-dirname" }, { - "description": "core:path:deny-normalize -> Denies the normalize command without any pre-configured scope.", + "description": "Denies the extname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-normalize" - ] + "const": "core:path:deny-extname" }, { - "description": "core:path:deny-resolve -> Denies the resolve command without any pre-configured scope.", + "description": "Denies the is_absolute command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-resolve" - ] + "const": "core:path:deny-is-absolute" }, { - "description": "core:path:deny-resolve-directory -> Denies the resolve_directory command without any pre-configured scope.", + "description": "Denies the join command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-resolve-directory" - ] + "const": "core:path:deny-join" }, { - "description": "core:resources:default -> Default permissions for the plugin.", + "description": "Denies the normalize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:resources:default" - ] + "const": "core:path:deny-normalize" }, { - "description": "core:resources:allow-close -> Enables the close command without any pre-configured scope.", + "description": "Denies the resolve command without any pre-configured scope.", "type": "string", - "enum": [ - "core:resources:allow-close" - ] + "const": "core:path:deny-resolve" }, { - "description": "core:resources:deny-close -> Denies the close command without any pre-configured scope.", + "description": "Denies the resolve_directory command without any pre-configured scope.", "type": "string", - "enum": [ - "core:resources:deny-close" - ] + "const": "core:path:deny-resolve-directory" }, { - "description": "core:tray:default -> Default permissions for the plugin.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:tray:default" - ] + "const": "core:resources:default" }, { - "description": "core:tray:allow-get-by-id -> Enables the get_by_id command without any pre-configured scope.", + "description": "Enables the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-get-by-id" - ] + "const": "core:resources:allow-close" }, { - "description": "core:tray:allow-new -> Enables the new command without any pre-configured scope.", + "description": "Denies the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-new" - ] + "const": "core:resources:deny-close" }, { - "description": "core:tray:allow-remove-by-id -> Enables the remove_by_id command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:tray:allow-remove-by-id" - ] + "const": "core:tray:default" }, { - "description": "core:tray:allow-set-icon -> Enables the set_icon command without any pre-configured scope.", + "description": "Enables the get_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-icon" - ] + "const": "core:tray:allow-get-by-id" }, { - "description": "core:tray:allow-set-icon-as-template -> Enables the set_icon_as_template command without any pre-configured scope.", + "description": "Enables the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-icon-as-template" - ] + "const": "core:tray:allow-new" }, { - "description": "core:tray:allow-set-menu -> Enables the set_menu command without any pre-configured scope.", + "description": "Enables the remove_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-menu" - ] + "const": "core:tray:allow-remove-by-id" }, { - "description": "core:tray:allow-set-show-menu-on-left-click -> Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-show-menu-on-left-click" - ] + "const": "core:tray:allow-set-icon" }, { - "description": "core:tray:allow-set-temp-dir-path -> Enables the set_temp_dir_path command without any pre-configured scope.", + "description": "Enables the set_icon_as_template command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-temp-dir-path" - ] + "const": "core:tray:allow-set-icon-as-template" }, { - "description": "core:tray:allow-set-title -> Enables the set_title command without any pre-configured scope.", + "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-title" - ] + "const": "core:tray:allow-set-menu" }, { - "description": "core:tray:allow-set-tooltip -> Enables the set_tooltip command without any pre-configured scope.", + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-tooltip" - ] + "const": "core:tray:allow-set-show-menu-on-left-click" }, { - "description": "core:tray:allow-set-visible -> Enables the set_visible command without any pre-configured scope.", + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-visible" - ] + "const": "core:tray:allow-set-temp-dir-path" }, { - "description": "core:tray:deny-get-by-id -> Denies the get_by_id command without any pre-configured scope.", + "description": "Enables the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-get-by-id" - ] + "const": "core:tray:allow-set-title" }, { - "description": "core:tray:deny-new -> Denies the new command without any pre-configured scope.", + "description": "Enables the set_tooltip command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-new" - ] + "const": "core:tray:allow-set-tooltip" }, { - "description": "core:tray:deny-remove-by-id -> Denies the remove_by_id command without any pre-configured scope.", + "description": "Enables the set_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-remove-by-id" - ] + "const": "core:tray:allow-set-visible" }, { - "description": "core:tray:deny-set-icon -> Denies the set_icon command without any pre-configured scope.", + "description": "Denies the get_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-icon" - ] + "const": "core:tray:deny-get-by-id" }, { - "description": "core:tray:deny-set-icon-as-template -> Denies the set_icon_as_template command without any pre-configured scope.", + "description": "Denies the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-icon-as-template" - ] + "const": "core:tray:deny-new" }, { - "description": "core:tray:deny-set-menu -> Denies the set_menu command without any pre-configured scope.", + "description": "Denies the remove_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-menu" - ] + "const": "core:tray:deny-remove-by-id" }, { - "description": "core:tray:deny-set-show-menu-on-left-click -> Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-show-menu-on-left-click" - ] + "const": "core:tray:deny-set-icon" }, { - "description": "core:tray:deny-set-temp-dir-path -> Denies the set_temp_dir_path command without any pre-configured scope.", + "description": "Denies the set_icon_as_template command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-temp-dir-path" - ] + "const": "core:tray:deny-set-icon-as-template" }, { - "description": "core:tray:deny-set-title -> Denies the set_title command without any pre-configured scope.", + "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-title" - ] + "const": "core:tray:deny-set-menu" }, { - "description": "core:tray:deny-set-tooltip -> Denies the set_tooltip command without any pre-configured scope.", + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-tooltip" - ] + "const": "core:tray:deny-set-show-menu-on-left-click" }, { - "description": "core:tray:deny-set-visible -> Denies the set_visible command without any pre-configured scope.", + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-visible" - ] + "const": "core:tray:deny-set-temp-dir-path" }, { - "description": "core:webview:default -> Default permissions for the plugin.", + "description": "Denies the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:default" - ] + "const": "core:tray:deny-set-title" }, { - "description": "core:webview:allow-create-webview -> Enables the create_webview command without any pre-configured scope.", + "description": "Denies the set_tooltip command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-create-webview" - ] + "const": "core:tray:deny-set-tooltip" }, { - "description": "core:webview:allow-create-webview-window -> Enables the create_webview_window command without any pre-configured scope.", + "description": "Denies the set_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-create-webview-window" - ] + "const": "core:tray:deny-set-visible" }, { - "description": "core:webview:allow-get-all-webviews -> Enables the get_all_webviews command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:webview:allow-get-all-webviews" - ] + "const": "core:webview:default" }, { - "description": "core:webview:allow-internal-toggle-devtools -> Enables the internal_toggle_devtools command without any pre-configured scope.", + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-internal-toggle-devtools" - ] + "const": "core:webview:allow-clear-all-browsing-data" }, { - "description": "core:webview:allow-print -> Enables the print command without any pre-configured scope.", + "description": "Enables the create_webview command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-print" - ] + "const": "core:webview:allow-create-webview" }, { - "description": "core:webview:allow-reparent -> Enables the reparent command without any pre-configured scope.", + "description": "Enables the create_webview_window command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-reparent" - ] + "const": "core:webview:allow-create-webview-window" }, { - "description": "core:webview:allow-set-webview-focus -> Enables the set_webview_focus command without any pre-configured scope.", + "description": "Enables the get_all_webviews command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-focus" - ] + "const": "core:webview:allow-get-all-webviews" }, { - "description": "core:webview:allow-set-webview-position -> Enables the set_webview_position command without any pre-configured scope.", + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-position" - ] + "const": "core:webview:allow-internal-toggle-devtools" }, { - "description": "core:webview:allow-set-webview-size -> Enables the set_webview_size command without any pre-configured scope.", + "description": "Enables the print command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-size" - ] + "const": "core:webview:allow-print" }, { - "description": "core:webview:allow-set-webview-zoom -> Enables the set_webview_zoom command without any pre-configured scope.", + "description": "Enables the reparent command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-zoom" - ] + "const": "core:webview:allow-reparent" }, { - "description": "core:webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.", + "description": "Enables the set_webview_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-webview-close" - ] + "const": "core:webview:allow-set-webview-focus" }, { - "description": "core:webview:allow-webview-position -> Enables the webview_position command without any pre-configured scope.", + "description": "Enables the set_webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-webview-position" - ] + "const": "core:webview:allow-set-webview-position" }, { - "description": "core:webview:allow-webview-size -> Enables the webview_size command without any pre-configured scope.", + "description": "Enables the set_webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-webview-size" - ] + "const": "core:webview:allow-set-webview-size" }, { - "description": "core:webview:deny-create-webview -> Denies the create_webview command without any pre-configured scope.", + "description": "Enables the set_webview_zoom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-create-webview" - ] + "const": "core:webview:allow-set-webview-zoom" }, { - "description": "core:webview:deny-create-webview-window -> Denies the create_webview_window command without any pre-configured scope.", + "description": "Enables the webview_close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-create-webview-window" - ] + "const": "core:webview:allow-webview-close" }, { - "description": "core:webview:deny-get-all-webviews -> Denies the get_all_webviews command without any pre-configured scope.", + "description": "Enables the webview_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-get-all-webviews" - ] + "const": "core:webview:allow-webview-hide" }, { - "description": "core:webview:deny-internal-toggle-devtools -> Denies the internal_toggle_devtools command without any pre-configured scope.", + "description": "Enables the webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-internal-toggle-devtools" - ] + "const": "core:webview:allow-webview-position" }, { - "description": "core:webview:deny-print -> Denies the print command without any pre-configured scope.", + "description": "Enables the webview_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-print" - ] + "const": "core:webview:allow-webview-show" }, { - "description": "core:webview:deny-reparent -> Denies the reparent command without any pre-configured scope.", + "description": "Enables the webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-reparent" - ] + "const": "core:webview:allow-webview-size" }, { - "description": "core:webview:deny-set-webview-focus -> Denies the set_webview_focus command without any pre-configured scope.", + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-focus" - ] + "const": "core:webview:deny-clear-all-browsing-data" }, { - "description": "core:webview:deny-set-webview-position -> Denies the set_webview_position command without any pre-configured scope.", + "description": "Denies the create_webview command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-position" - ] + "const": "core:webview:deny-create-webview" }, { - "description": "core:webview:deny-set-webview-size -> Denies the set_webview_size command without any pre-configured scope.", + "description": "Denies the create_webview_window command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-size" - ] + "const": "core:webview:deny-create-webview-window" }, { - "description": "core:webview:deny-set-webview-zoom -> Denies the set_webview_zoom command without any pre-configured scope.", + "description": "Denies the get_all_webviews command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-zoom" - ] + "const": "core:webview:deny-get-all-webviews" }, { - "description": "core:webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.", + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-webview-close" - ] + "const": "core:webview:deny-internal-toggle-devtools" }, { - "description": "core:webview:deny-webview-position -> Denies the webview_position command without any pre-configured scope.", + "description": "Denies the print command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-webview-position" - ] + "const": "core:webview:deny-print" }, { - "description": "core:webview:deny-webview-size -> Denies the webview_size command without any pre-configured scope.", + "description": "Denies the reparent command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-webview-size" - ] + "const": "core:webview:deny-reparent" }, { - "description": "core:window:default -> Default permissions for the plugin.", + "description": "Denies the set_webview_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:default" - ] + "const": "core:webview:deny-set-webview-focus" }, { - "description": "core:window:allow-available-monitors -> Enables the available_monitors command without any pre-configured scope.", + "description": "Denies the set_webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-available-monitors" - ] + "const": "core:webview:deny-set-webview-position" }, { - "description": "core:window:allow-center -> Enables the center command without any pre-configured scope.", + "description": "Denies the set_webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-center" - ] + "const": "core:webview:deny-set-webview-size" }, { - "description": "core:window:allow-close -> Enables the close command without any pre-configured scope.", + "description": "Denies the set_webview_zoom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-close" - ] + "const": "core:webview:deny-set-webview-zoom" }, { - "description": "core:window:allow-create -> Enables the create command without any pre-configured scope.", + "description": "Denies the webview_close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-create" - ] + "const": "core:webview:deny-webview-close" }, { - "description": "core:window:allow-current-monitor -> Enables the current_monitor command without any pre-configured scope.", + "description": "Denies the webview_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-current-monitor" - ] + "const": "core:webview:deny-webview-hide" }, { - "description": "core:window:allow-cursor-position -> Enables the cursor_position command without any pre-configured scope.", + "description": "Denies the webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-cursor-position" - ] + "const": "core:webview:deny-webview-position" }, { - "description": "core:window:allow-destroy -> Enables the destroy command without any pre-configured scope.", + "description": "Denies the webview_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-destroy" - ] + "const": "core:webview:deny-webview-show" }, { - "description": "core:window:allow-get-all-windows -> Enables the get_all_windows command without any pre-configured scope.", + "description": "Denies the webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-get-all-windows" - ] + "const": "core:webview:deny-webview-size" }, { - "description": "core:window:allow-hide -> Enables the hide command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:window:allow-hide" - ] + "const": "core:window:default" }, { - "description": "core:window:allow-inner-position -> Enables the inner_position command without any pre-configured scope.", + "description": "Enables the available_monitors command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-inner-position" - ] + "const": "core:window:allow-available-monitors" }, { - "description": "core:window:allow-inner-size -> Enables the inner_size command without any pre-configured scope.", + "description": "Enables the center command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-inner-size" - ] + "const": "core:window:allow-center" }, { - "description": "core:window:allow-internal-toggle-maximize -> Enables the internal_toggle_maximize command without any pre-configured scope.", + "description": "Enables the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-internal-toggle-maximize" - ] + "const": "core:window:allow-close" }, { - "description": "core:window:allow-is-closable -> Enables the is_closable command without any pre-configured scope.", + "description": "Enables the create command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-closable" - ] + "const": "core:window:allow-create" }, { - "description": "core:window:allow-is-decorated -> Enables the is_decorated command without any pre-configured scope.", + "description": "Enables the current_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-decorated" - ] + "const": "core:window:allow-current-monitor" }, { - "description": "core:window:allow-is-focused -> Enables the is_focused command without any pre-configured scope.", + "description": "Enables the cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-focused" - ] + "const": "core:window:allow-cursor-position" }, { - "description": "core:window:allow-is-fullscreen -> Enables the is_fullscreen command without any pre-configured scope.", + "description": "Enables the destroy command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-fullscreen" - ] + "const": "core:window:allow-destroy" }, { - "description": "core:window:allow-is-maximizable -> Enables the is_maximizable command without any pre-configured scope.", + "description": "Enables the get_all_windows command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-maximizable" - ] + "const": "core:window:allow-get-all-windows" }, { - "description": "core:window:allow-is-maximized -> Enables the is_maximized command without any pre-configured scope.", + "description": "Enables the hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-maximized" - ] + "const": "core:window:allow-hide" }, { - "description": "core:window:allow-is-minimizable -> Enables the is_minimizable command without any pre-configured scope.", + "description": "Enables the inner_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-minimizable" - ] + "const": "core:window:allow-inner-position" }, { - "description": "core:window:allow-is-minimized -> Enables the is_minimized command without any pre-configured scope.", + "description": "Enables the inner_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-minimized" - ] + "const": "core:window:allow-inner-size" }, { - "description": "core:window:allow-is-resizable -> Enables the is_resizable command without any pre-configured scope.", + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-resizable" - ] + "const": "core:window:allow-internal-toggle-maximize" }, { - "description": "core:window:allow-is-visible -> Enables the is_visible command without any pre-configured scope.", + "description": "Enables the is_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-visible" - ] + "const": "core:window:allow-is-closable" }, { - "description": "core:window:allow-maximize -> Enables the maximize command without any pre-configured scope.", + "description": "Enables the is_decorated command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-maximize" - ] + "const": "core:window:allow-is-decorated" }, { - "description": "core:window:allow-minimize -> Enables the minimize command without any pre-configured scope.", + "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-minimize" - ] + "const": "core:window:allow-is-enabled" }, { - "description": "core:window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.", + "description": "Enables the is_focused command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-monitor-from-point" - ] + "const": "core:window:allow-is-focused" }, { - "description": "core:window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", + "description": "Enables the is_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-outer-position" - ] + "const": "core:window:allow-is-fullscreen" }, { - "description": "core:window:allow-outer-size -> Enables the outer_size command without any pre-configured scope.", + "description": "Enables the is_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-outer-size" - ] + "const": "core:window:allow-is-maximizable" }, { - "description": "core:window:allow-primary-monitor -> Enables the primary_monitor command without any pre-configured scope.", + "description": "Enables the is_maximized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-primary-monitor" - ] + "const": "core:window:allow-is-maximized" }, { - "description": "core:window:allow-request-user-attention -> Enables the request_user_attention command without any pre-configured scope.", + "description": "Enables the is_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-request-user-attention" - ] + "const": "core:window:allow-is-minimizable" }, { - "description": "core:window:allow-scale-factor -> Enables the scale_factor command without any pre-configured scope.", + "description": "Enables the is_minimized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-scale-factor" - ] + "const": "core:window:allow-is-minimized" }, { - "description": "core:window:allow-set-always-on-bottom -> Enables the set_always_on_bottom command without any pre-configured scope.", + "description": "Enables the is_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-always-on-bottom" - ] + "const": "core:window:allow-is-resizable" }, { - "description": "core:window:allow-set-always-on-top -> Enables the set_always_on_top command without any pre-configured scope.", + "description": "Enables the is_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-always-on-top" - ] + "const": "core:window:allow-is-visible" }, { - "description": "core:window:allow-set-closable -> Enables the set_closable command without any pre-configured scope.", + "description": "Enables the maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-closable" - ] + "const": "core:window:allow-maximize" }, { - "description": "core:window:allow-set-content-protected -> Enables the set_content_protected command without any pre-configured scope.", + "description": "Enables the minimize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-content-protected" - ] + "const": "core:window:allow-minimize" }, { - "description": "core:window:allow-set-cursor-grab -> Enables the set_cursor_grab command without any pre-configured scope.", + "description": "Enables the monitor_from_point command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-grab" - ] + "const": "core:window:allow-monitor-from-point" }, { - "description": "core:window:allow-set-cursor-icon -> Enables the set_cursor_icon command without any pre-configured scope.", + "description": "Enables the outer_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-icon" - ] + "const": "core:window:allow-outer-position" }, { - "description": "core:window:allow-set-cursor-position -> Enables the set_cursor_position command without any pre-configured scope.", + "description": "Enables the outer_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-position" - ] + "const": "core:window:allow-outer-size" }, { - "description": "core:window:allow-set-cursor-visible -> Enables the set_cursor_visible command without any pre-configured scope.", + "description": "Enables the primary_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-visible" - ] + "const": "core:window:allow-primary-monitor" }, { - "description": "core:window:allow-set-decorations -> Enables the set_decorations command without any pre-configured scope.", + "description": "Enables the request_user_attention command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-decorations" - ] + "const": "core:window:allow-request-user-attention" }, { - "description": "core:window:allow-set-effects -> Enables the set_effects command without any pre-configured scope.", + "description": "Enables the scale_factor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-effects" - ] + "const": "core:window:allow-scale-factor" }, { - "description": "core:window:allow-set-focus -> Enables the set_focus command without any pre-configured scope.", + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-focus" - ] + "const": "core:window:allow-set-always-on-bottom" }, { - "description": "core:window:allow-set-fullscreen -> Enables the set_fullscreen command without any pre-configured scope.", + "description": "Enables the set_always_on_top command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-fullscreen" - ] + "const": "core:window:allow-set-always-on-top" }, { - "description": "core:window:allow-set-icon -> Enables the set_icon command without any pre-configured scope.", + "description": "Enables the set_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-icon" - ] + "const": "core:window:allow-set-closable" }, { - "description": "core:window:allow-set-ignore-cursor-events -> Enables the set_ignore_cursor_events command without any pre-configured scope.", + "description": "Enables the set_content_protected command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-ignore-cursor-events" - ] + "const": "core:window:allow-set-content-protected" }, { - "description": "core:window:allow-set-max-size -> Enables the set_max_size command without any pre-configured scope.", + "description": "Enables the set_cursor_grab command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-max-size" - ] + "const": "core:window:allow-set-cursor-grab" }, { - "description": "core:window:allow-set-maximizable -> Enables the set_maximizable command without any pre-configured scope.", + "description": "Enables the set_cursor_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-maximizable" - ] + "const": "core:window:allow-set-cursor-icon" }, { - "description": "core:window:allow-set-min-size -> Enables the set_min_size command without any pre-configured scope.", + "description": "Enables the set_cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-min-size" - ] + "const": "core:window:allow-set-cursor-position" }, { - "description": "core:window:allow-set-minimizable -> Enables the set_minimizable command without any pre-configured scope.", + "description": "Enables the set_cursor_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-minimizable" - ] + "const": "core:window:allow-set-cursor-visible" }, { - "description": "core:window:allow-set-position -> Enables the set_position command without any pre-configured scope.", + "description": "Enables the set_decorations command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-position" - ] + "const": "core:window:allow-set-decorations" }, { - "description": "core:window:allow-set-progress-bar -> Enables the set_progress_bar command without any pre-configured scope.", + "description": "Enables the set_effects command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-progress-bar" - ] + "const": "core:window:allow-set-effects" }, { - "description": "core:window:allow-set-resizable -> Enables the set_resizable command without any pre-configured scope.", + "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-resizable" - ] + "const": "core:window:allow-set-enabled" }, { - "description": "core:window:allow-set-shadow -> Enables the set_shadow command without any pre-configured scope.", + "description": "Enables the set_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-shadow" - ] + "const": "core:window:allow-set-focus" }, { - "description": "core:window:allow-set-size -> Enables the set_size command without any pre-configured scope.", + "description": "Enables the set_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-size" - ] + "const": "core:window:allow-set-fullscreen" }, { - "description": "core:window:allow-set-size-constraints -> Enables the set_size_constraints command without any pre-configured scope.", + "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-size-constraints" - ] + "const": "core:window:allow-set-icon" }, { - "description": "core:window:allow-set-skip-taskbar -> Enables the set_skip_taskbar command without any pre-configured scope.", + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-skip-taskbar" - ] + "const": "core:window:allow-set-ignore-cursor-events" }, { - "description": "core:window:allow-set-title -> Enables the set_title command without any pre-configured scope.", + "description": "Enables the set_max_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-title" - ] + "const": "core:window:allow-set-max-size" }, { - "description": "core:window:allow-set-title-bar-style -> Enables the set_title_bar_style command without any pre-configured scope.", + "description": "Enables the set_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-title-bar-style" - ] + "const": "core:window:allow-set-maximizable" }, { - "description": "core:window:allow-set-visible-on-all-workspaces -> Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "description": "Enables the set_min_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-visible-on-all-workspaces" - ] + "const": "core:window:allow-set-min-size" }, { - "description": "core:window:allow-show -> Enables the show command without any pre-configured scope.", + "description": "Enables the set_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-show" - ] + "const": "core:window:allow-set-minimizable" }, { - "description": "core:window:allow-start-dragging -> Enables the start_dragging command without any pre-configured scope.", + "description": "Enables the set_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-start-dragging" - ] + "const": "core:window:allow-set-position" }, { - "description": "core:window:allow-start-resize-dragging -> Enables the start_resize_dragging command without any pre-configured scope.", + "description": "Enables the set_progress_bar command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-start-resize-dragging" - ] + "const": "core:window:allow-set-progress-bar" }, { - "description": "core:window:allow-theme -> Enables the theme command without any pre-configured scope.", + "description": "Enables the set_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-theme" - ] + "const": "core:window:allow-set-resizable" }, { - "description": "core:window:allow-title -> Enables the title command without any pre-configured scope.", + "description": "Enables the set_shadow command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-title" - ] + "const": "core:window:allow-set-shadow" }, { - "description": "core:window:allow-toggle-maximize -> Enables the toggle_maximize command without any pre-configured scope.", + "description": "Enables the set_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-toggle-maximize" - ] + "const": "core:window:allow-set-size" }, { - "description": "core:window:allow-unmaximize -> Enables the unmaximize command without any pre-configured scope.", + "description": "Enables the set_size_constraints command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-unmaximize" - ] + "const": "core:window:allow-set-size-constraints" }, { - "description": "core:window:allow-unminimize -> Enables the unminimize command without any pre-configured scope.", + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-unminimize" - ] + "const": "core:window:allow-set-skip-taskbar" }, { - "description": "core:window:deny-available-monitors -> Denies the available_monitors command without any pre-configured scope.", + "description": "Enables the set_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-available-monitors" - ] + "const": "core:window:allow-set-theme" }, { - "description": "core:window:deny-center -> Denies the center command without any pre-configured scope.", + "description": "Enables the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-center" - ] + "const": "core:window:allow-set-title" }, { - "description": "core:window:deny-close -> Denies the close command without any pre-configured scope.", + "description": "Enables the set_title_bar_style command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-close" - ] + "const": "core:window:allow-set-title-bar-style" }, { - "description": "core:window:deny-create -> Denies the create command without any pre-configured scope.", + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-create" - ] + "const": "core:window:allow-set-visible-on-all-workspaces" }, { - "description": "core:window:deny-current-monitor -> Denies the current_monitor command without any pre-configured scope.", + "description": "Enables the show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-current-monitor" - ] + "const": "core:window:allow-show" }, { - "description": "core:window:deny-cursor-position -> Denies the cursor_position command without any pre-configured scope.", + "description": "Enables the start_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-cursor-position" - ] + "const": "core:window:allow-start-dragging" }, { - "description": "core:window:deny-destroy -> Denies the destroy command without any pre-configured scope.", + "description": "Enables the start_resize_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-destroy" - ] + "const": "core:window:allow-start-resize-dragging" }, { - "description": "core:window:deny-get-all-windows -> Denies the get_all_windows command without any pre-configured scope.", + "description": "Enables the theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-get-all-windows" - ] + "const": "core:window:allow-theme" }, { - "description": "core:window:deny-hide -> Denies the hide command without any pre-configured scope.", + "description": "Enables the title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-hide" - ] + "const": "core:window:allow-title" }, { - "description": "core:window:deny-inner-position -> Denies the inner_position command without any pre-configured scope.", + "description": "Enables the toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-inner-position" - ] + "const": "core:window:allow-toggle-maximize" }, { - "description": "core:window:deny-inner-size -> Denies the inner_size command without any pre-configured scope.", + "description": "Enables the unmaximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-inner-size" - ] + "const": "core:window:allow-unmaximize" }, { - "description": "core:window:deny-internal-toggle-maximize -> Denies the internal_toggle_maximize command without any pre-configured scope.", + "description": "Enables the unminimize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-internal-toggle-maximize" - ] + "const": "core:window:allow-unminimize" }, { - "description": "core:window:deny-is-closable -> Denies the is_closable command without any pre-configured scope.", + "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-closable" - ] + "const": "core:window:deny-available-monitors" }, { - "description": "core:window:deny-is-decorated -> Denies the is_decorated command without any pre-configured scope.", + "description": "Denies the center command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-decorated" - ] + "const": "core:window:deny-center" }, { - "description": "core:window:deny-is-focused -> Denies the is_focused command without any pre-configured scope.", + "description": "Denies the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-focused" - ] + "const": "core:window:deny-close" }, { - "description": "core:window:deny-is-fullscreen -> Denies the is_fullscreen command without any pre-configured scope.", + "description": "Denies the create command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-fullscreen" - ] + "const": "core:window:deny-create" }, { - "description": "core:window:deny-is-maximizable -> Denies the is_maximizable command without any pre-configured scope.", + "description": "Denies the current_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-maximizable" - ] + "const": "core:window:deny-current-monitor" }, { - "description": "core:window:deny-is-maximized -> Denies the is_maximized command without any pre-configured scope.", + "description": "Denies the cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-maximized" - ] + "const": "core:window:deny-cursor-position" }, { - "description": "core:window:deny-is-minimizable -> Denies the is_minimizable command without any pre-configured scope.", + "description": "Denies the destroy command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-minimizable" - ] + "const": "core:window:deny-destroy" }, { - "description": "core:window:deny-is-minimized -> Denies the is_minimized command without any pre-configured scope.", + "description": "Denies the get_all_windows command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-minimized" - ] + "const": "core:window:deny-get-all-windows" }, { - "description": "core:window:deny-is-resizable -> Denies the is_resizable command without any pre-configured scope.", + "description": "Denies the hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-resizable" - ] + "const": "core:window:deny-hide" }, { - "description": "core:window:deny-is-visible -> Denies the is_visible command without any pre-configured scope.", + "description": "Denies the inner_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-visible" - ] + "const": "core:window:deny-inner-position" }, { - "description": "core:window:deny-maximize -> Denies the maximize command without any pre-configured scope.", + "description": "Denies the inner_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-maximize" - ] + "const": "core:window:deny-inner-size" }, { - "description": "core:window:deny-minimize -> Denies the minimize command without any pre-configured scope.", + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-minimize" - ] + "const": "core:window:deny-internal-toggle-maximize" }, { - "description": "core:window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.", + "description": "Denies the is_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-monitor-from-point" - ] + "const": "core:window:deny-is-closable" }, { - "description": "core:window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", + "description": "Denies the is_decorated command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-outer-position" - ] + "const": "core:window:deny-is-decorated" }, { - "description": "core:window:deny-outer-size -> Denies the outer_size command without any pre-configured scope.", + "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-outer-size" - ] + "const": "core:window:deny-is-enabled" }, { - "description": "core:window:deny-primary-monitor -> Denies the primary_monitor command without any pre-configured scope.", + "description": "Denies the is_focused command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-primary-monitor" - ] + "const": "core:window:deny-is-focused" }, { - "description": "core:window:deny-request-user-attention -> Denies the request_user_attention command without any pre-configured scope.", + "description": "Denies the is_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-request-user-attention" - ] + "const": "core:window:deny-is-fullscreen" }, { - "description": "core:window:deny-scale-factor -> Denies the scale_factor command without any pre-configured scope.", + "description": "Denies the is_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-scale-factor" - ] + "const": "core:window:deny-is-maximizable" }, { - "description": "core:window:deny-set-always-on-bottom -> Denies the set_always_on_bottom command without any pre-configured scope.", + "description": "Denies the is_maximized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-always-on-bottom" - ] + "const": "core:window:deny-is-maximized" }, { - "description": "core:window:deny-set-always-on-top -> Denies the set_always_on_top command without any pre-configured scope.", + "description": "Denies the is_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-always-on-top" - ] + "const": "core:window:deny-is-minimizable" }, { - "description": "core:window:deny-set-closable -> Denies the set_closable command without any pre-configured scope.", + "description": "Denies the is_minimized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-closable" - ] + "const": "core:window:deny-is-minimized" }, { - "description": "core:window:deny-set-content-protected -> Denies the set_content_protected command without any pre-configured scope.", + "description": "Denies the is_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-content-protected" - ] + "const": "core:window:deny-is-resizable" }, { - "description": "core:window:deny-set-cursor-grab -> Denies the set_cursor_grab command without any pre-configured scope.", + "description": "Denies the is_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-grab" - ] + "const": "core:window:deny-is-visible" }, { - "description": "core:window:deny-set-cursor-icon -> Denies the set_cursor_icon command without any pre-configured scope.", + "description": "Denies the maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-icon" - ] + "const": "core:window:deny-maximize" }, { - "description": "core:window:deny-set-cursor-position -> Denies the set_cursor_position command without any pre-configured scope.", + "description": "Denies the minimize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-position" - ] + "const": "core:window:deny-minimize" }, { - "description": "core:window:deny-set-cursor-visible -> Denies the set_cursor_visible command without any pre-configured scope.", + "description": "Denies the monitor_from_point command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-visible" - ] + "const": "core:window:deny-monitor-from-point" }, { - "description": "core:window:deny-set-decorations -> Denies the set_decorations command without any pre-configured scope.", + "description": "Denies the outer_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-decorations" - ] + "const": "core:window:deny-outer-position" }, { - "description": "core:window:deny-set-effects -> Denies the set_effects command without any pre-configured scope.", + "description": "Denies the outer_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-effects" - ] + "const": "core:window:deny-outer-size" }, { - "description": "core:window:deny-set-focus -> Denies the set_focus command without any pre-configured scope.", + "description": "Denies the primary_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-focus" - ] + "const": "core:window:deny-primary-monitor" }, { - "description": "core:window:deny-set-fullscreen -> Denies the set_fullscreen command without any pre-configured scope.", + "description": "Denies the request_user_attention command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-fullscreen" - ] + "const": "core:window:deny-request-user-attention" }, { - "description": "core:window:deny-set-icon -> Denies the set_icon command without any pre-configured scope.", + "description": "Denies the scale_factor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-icon" - ] + "const": "core:window:deny-scale-factor" }, { - "description": "core:window:deny-set-ignore-cursor-events -> Denies the set_ignore_cursor_events command without any pre-configured scope.", + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-ignore-cursor-events" - ] + "const": "core:window:deny-set-always-on-bottom" }, { - "description": "core:window:deny-set-max-size -> Denies the set_max_size command without any pre-configured scope.", + "description": "Denies the set_always_on_top command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-max-size" - ] + "const": "core:window:deny-set-always-on-top" }, { - "description": "core:window:deny-set-maximizable -> Denies the set_maximizable command without any pre-configured scope.", + "description": "Denies the set_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-maximizable" - ] + "const": "core:window:deny-set-closable" }, { - "description": "core:window:deny-set-min-size -> Denies the set_min_size command without any pre-configured scope.", + "description": "Denies the set_content_protected command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-min-size" - ] + "const": "core:window:deny-set-content-protected" }, { - "description": "core:window:deny-set-minimizable -> Denies the set_minimizable command without any pre-configured scope.", + "description": "Denies the set_cursor_grab command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-minimizable" - ] + "const": "core:window:deny-set-cursor-grab" }, { - "description": "core:window:deny-set-position -> Denies the set_position command without any pre-configured scope.", + "description": "Denies the set_cursor_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-position" - ] + "const": "core:window:deny-set-cursor-icon" }, { - "description": "core:window:deny-set-progress-bar -> Denies the set_progress_bar command without any pre-configured scope.", + "description": "Denies the set_cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-progress-bar" - ] + "const": "core:window:deny-set-cursor-position" }, { - "description": "core:window:deny-set-resizable -> Denies the set_resizable command without any pre-configured scope.", + "description": "Denies the set_cursor_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-resizable" - ] + "const": "core:window:deny-set-cursor-visible" }, { - "description": "core:window:deny-set-shadow -> Denies the set_shadow command without any pre-configured scope.", + "description": "Denies the set_decorations command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-shadow" - ] + "const": "core:window:deny-set-decorations" }, { - "description": "core:window:deny-set-size -> Denies the set_size command without any pre-configured scope.", + "description": "Denies the set_effects command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-size" - ] + "const": "core:window:deny-set-effects" }, { - "description": "core:window:deny-set-size-constraints -> Denies the set_size_constraints command without any pre-configured scope.", + "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-size-constraints" - ] + "const": "core:window:deny-set-enabled" }, { - "description": "core:window:deny-set-skip-taskbar -> Denies the set_skip_taskbar command without any pre-configured scope.", + "description": "Denies the set_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-skip-taskbar" - ] + "const": "core:window:deny-set-focus" }, { - "description": "core:window:deny-set-title -> Denies the set_title command without any pre-configured scope.", + "description": "Denies the set_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-title" - ] + "const": "core:window:deny-set-fullscreen" }, { - "description": "core:window:deny-set-title-bar-style -> Denies the set_title_bar_style command without any pre-configured scope.", + "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-title-bar-style" - ] + "const": "core:window:deny-set-icon" }, { - "description": "core:window:deny-set-visible-on-all-workspaces -> Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-visible-on-all-workspaces" - ] + "const": "core:window:deny-set-ignore-cursor-events" }, { - "description": "core:window:deny-show -> Denies the show command without any pre-configured scope.", + "description": "Denies the set_max_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-show" - ] + "const": "core:window:deny-set-max-size" }, { - "description": "core:window:deny-start-dragging -> Denies the start_dragging command without any pre-configured scope.", + "description": "Denies the set_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-start-dragging" - ] + "const": "core:window:deny-set-maximizable" }, { - "description": "core:window:deny-start-resize-dragging -> Denies the start_resize_dragging command without any pre-configured scope.", + "description": "Denies the set_min_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-start-resize-dragging" - ] + "const": "core:window:deny-set-min-size" }, { - "description": "core:window:deny-theme -> Denies the theme command without any pre-configured scope.", + "description": "Denies the set_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-theme" - ] + "const": "core:window:deny-set-minimizable" }, { - "description": "core:window:deny-title -> Denies the title command without any pre-configured scope.", + "description": "Denies the set_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-title" - ] + "const": "core:window:deny-set-position" }, { - "description": "core:window:deny-toggle-maximize -> Denies the toggle_maximize command without any pre-configured scope.", + "description": "Denies the set_progress_bar command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-toggle-maximize" - ] + "const": "core:window:deny-set-progress-bar" }, { - "description": "core:window:deny-unmaximize -> Denies the unmaximize command without any pre-configured scope.", + "description": "Denies the set_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-unmaximize" - ] + "const": "core:window:deny-set-resizable" }, { - "description": "core:window:deny-unminimize -> Denies the unminimize command without any pre-configured scope.", + "description": "Denies the set_shadow command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-unminimize" - ] + "const": "core:window:deny-set-shadow" }, { - "description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n", + "description": "Denies the set_size command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:default" - ] + "const": "core:window:deny-set-size" }, { - "description": "dialog:allow-ask -> Enables the ask command without any pre-configured scope.", + "description": "Denies the set_size_constraints command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-ask" - ] + "const": "core:window:deny-set-size-constraints" }, { - "description": "dialog:allow-confirm -> Enables the confirm command without any pre-configured scope.", + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-confirm" - ] + "const": "core:window:deny-set-skip-taskbar" }, { - "description": "dialog:allow-message -> Enables the message command without any pre-configured scope.", + "description": "Denies the set_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-message" - ] + "const": "core:window:deny-set-theme" }, { - "description": "dialog:allow-open -> Enables the open command without any pre-configured scope.", + "description": "Denies the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-open" - ] + "const": "core:window:deny-set-title" }, { - "description": "dialog:allow-save -> Enables the save command without any pre-configured scope.", + "description": "Denies the set_title_bar_style command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-save" - ] + "const": "core:window:deny-set-title-bar-style" }, { - "description": "dialog:deny-ask -> Denies the ask command without any pre-configured scope.", + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-ask" - ] + "const": "core:window:deny-set-visible-on-all-workspaces" }, { - "description": "dialog:deny-confirm -> Denies the confirm command without any pre-configured scope.", + "description": "Denies the show command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-confirm" - ] + "const": "core:window:deny-show" }, { - "description": "dialog:deny-message -> Denies the message command without any pre-configured scope.", + "description": "Denies the start_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-message" - ] + "const": "core:window:deny-start-dragging" }, { - "description": "dialog:deny-open -> Denies the open command without any pre-configured scope.", + "description": "Denies the start_resize_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-open" - ] + "const": "core:window:deny-start-resize-dragging" }, { - "description": "dialog:deny-save -> Denies the save command without any pre-configured scope.", + "description": "Denies the theme command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-save" - ] + "const": "core:window:deny-theme" }, { - "description": "log:default -> Allows the log command", + "description": "Denies the title command without any pre-configured scope.", "type": "string", - "enum": [ - "log:default" - ] + "const": "core:window:deny-title" }, { - "description": "log:allow-log -> Enables the log command without any pre-configured scope.", + "description": "Denies the toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "log:allow-log" - ] + "const": "core:window:deny-toggle-maximize" }, { - "description": "log:deny-log -> Denies the log command without any pre-configured scope.", + "description": "Denies the unmaximize command without any pre-configured scope.", "type": "string", - "enum": [ - "log:deny-log" - ] + "const": "core:window:deny-unmaximize" }, { - "description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n", + "description": "Denies the unminimize command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:default" - ] + "const": "core:window:deny-unminimize" }, { - "description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.", + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n", "type": "string", - "enum": [ - "notification:allow-batch" - ] + "const": "dialog:default" }, { - "description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.", + "description": "Enables the ask command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-cancel" - ] + "const": "dialog:allow-ask" }, { - "description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.", + "description": "Enables the confirm command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-check-permissions" - ] + "const": "dialog:allow-confirm" }, { - "description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.", + "description": "Enables the message command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-create-channel" - ] + "const": "dialog:allow-message" }, { - "description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.", + "description": "Enables the open command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-delete-channel" - ] + "const": "dialog:allow-open" }, { - "description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.", + "description": "Enables the save command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-get-active" - ] + "const": "dialog:allow-save" }, { - "description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.", + "description": "Denies the ask command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-get-pending" - ] + "const": "dialog:deny-ask" }, { - "description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.", + "description": "Denies the confirm command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-is-permission-granted" - ] + "const": "dialog:deny-confirm" }, { - "description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.", + "description": "Denies the message command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-list-channels" - ] + "const": "dialog:deny-message" }, { - "description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.", + "description": "Denies the open command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-notify" - ] + "const": "dialog:deny-open" }, { - "description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.", + "description": "Denies the save command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-permission-state" - ] + "const": "dialog:deny-save" }, { - "description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.", + "description": "Allows the log command", "type": "string", - "enum": [ - "notification:allow-register-action-types" - ] + "const": "log:default" }, { - "description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.", + "description": "Enables the log command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-register-listener" - ] + "const": "log:allow-log" }, { - "description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.", + "description": "Denies the log command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-remove-active" - ] + "const": "log:deny-log" }, { - "description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.", + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n", "type": "string", - "enum": [ - "notification:allow-request-permission" - ] + "const": "notification:default" }, { - "description": "notification:allow-show -> Enables the show command without any pre-configured scope.", + "description": "Enables the batch command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-show" - ] + "const": "notification:allow-batch" }, { - "description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.", + "description": "Enables the cancel command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-batch" - ] + "const": "notification:allow-cancel" }, { - "description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.", + "description": "Enables the check_permissions command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-cancel" - ] + "const": "notification:allow-check-permissions" }, { - "description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.", + "description": "Enables the create_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-check-permissions" - ] + "const": "notification:allow-create-channel" }, { - "description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.", + "description": "Enables the delete_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-create-channel" - ] + "const": "notification:allow-delete-channel" }, { - "description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.", + "description": "Enables the get_active command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-delete-channel" - ] + "const": "notification:allow-get-active" }, { - "description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.", + "description": "Enables the get_pending command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-get-active" - ] + "const": "notification:allow-get-pending" }, { - "description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.", + "description": "Enables the is_permission_granted command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-get-pending" - ] + "const": "notification:allow-is-permission-granted" }, { - "description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.", + "description": "Enables the list_channels command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-is-permission-granted" - ] + "const": "notification:allow-list-channels" }, { - "description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.", + "description": "Enables the notify command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-list-channels" - ] + "const": "notification:allow-notify" }, { - "description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.", + "description": "Enables the permission_state command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-notify" - ] + "const": "notification:allow-permission-state" }, { - "description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.", + "description": "Enables the register_action_types command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-permission-state" - ] + "const": "notification:allow-register-action-types" }, { - "description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.", + "description": "Enables the register_listener command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-register-action-types" - ] + "const": "notification:allow-register-listener" }, { - "description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.", + "description": "Enables the remove_active command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-register-listener" - ] + "const": "notification:allow-remove-active" }, { - "description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.", + "description": "Enables the request_permission command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-remove-active" - ] + "const": "notification:allow-request-permission" }, { - "description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.", + "description": "Enables the show command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-request-permission" - ] + "const": "notification:allow-show" }, { - "description": "notification:deny-show -> Denies the show command without any pre-configured scope.", + "description": "Denies the batch command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-show" - ] + "const": "notification:deny-batch" }, { - "description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n", + "description": "Denies the cancel command without any pre-configured scope.", "type": "string", - "enum": [ - "os:default" - ] + "const": "notification:deny-cancel" }, { - "description": "os:allow-arch -> Enables the arch command without any pre-configured scope.", + "description": "Denies the check_permissions command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-arch" - ] + "const": "notification:deny-check-permissions" }, { - "description": "os:allow-exe-extension -> Enables the exe_extension command without any pre-configured scope.", + "description": "Denies the create_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-exe-extension" - ] + "const": "notification:deny-create-channel" }, { - "description": "os:allow-family -> Enables the family command without any pre-configured scope.", + "description": "Denies the delete_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-family" - ] + "const": "notification:deny-delete-channel" }, { - "description": "os:allow-hostname -> Enables the hostname command without any pre-configured scope.", + "description": "Denies the get_active command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-hostname" - ] + "const": "notification:deny-get-active" }, { - "description": "os:allow-locale -> Enables the locale command without any pre-configured scope.", + "description": "Denies the get_pending command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-locale" - ] + "const": "notification:deny-get-pending" }, { - "description": "os:allow-os-type -> Enables the os_type command without any pre-configured scope.", + "description": "Denies the is_permission_granted command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-os-type" - ] + "const": "notification:deny-is-permission-granted" }, { - "description": "os:allow-platform -> Enables the platform command without any pre-configured scope.", + "description": "Denies the list_channels command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-platform" - ] + "const": "notification:deny-list-channels" }, { - "description": "os:allow-version -> Enables the version command without any pre-configured scope.", + "description": "Denies the notify command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-version" - ] + "const": "notification:deny-notify" }, { - "description": "os:deny-arch -> Denies the arch command without any pre-configured scope.", + "description": "Denies the permission_state command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-arch" - ] + "const": "notification:deny-permission-state" }, { - "description": "os:deny-exe-extension -> Denies the exe_extension command without any pre-configured scope.", + "description": "Denies the register_action_types command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-exe-extension" - ] + "const": "notification:deny-register-action-types" }, { - "description": "os:deny-family -> Denies the family command without any pre-configured scope.", + "description": "Denies the register_listener command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-family" - ] + "const": "notification:deny-register-listener" }, { - "description": "os:deny-hostname -> Denies the hostname command without any pre-configured scope.", + "description": "Denies the remove_active command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-hostname" - ] + "const": "notification:deny-remove-active" }, { - "description": "os:deny-locale -> Denies the locale command without any pre-configured scope.", + "description": "Denies the request_permission command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-locale" - ] + "const": "notification:deny-request-permission" }, { - "description": "os:deny-os-type -> Denies the os_type command without any pre-configured scope.", + "description": "Denies the show command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-os-type" - ] + "const": "notification:deny-show" }, { - "description": "os:deny-platform -> Denies the platform command without any pre-configured scope.", + "description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n", "type": "string", - "enum": [ - "os:deny-platform" - ] + "const": "os:default" }, { - "description": "os:deny-version -> Denies the version command without any pre-configured scope.", + "description": "Enables the arch command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-version" - ] + "const": "os:allow-arch" }, { - "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", + "description": "Enables the exe_extension command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:default" - ] + "const": "os:allow-exe-extension" }, { - "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.", + "description": "Enables the family command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-execute" - ] + "const": "os:allow-family" }, { - "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.", + "description": "Enables the hostname command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-kill" - ] + "const": "os:allow-hostname" }, { - "description": "shell:allow-open -> Enables the open command without any pre-configured scope.", + "description": "Enables the locale command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-open" - ] + "const": "os:allow-locale" }, { - "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.", + "description": "Enables the os_type command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-spawn" - ] + "const": "os:allow-os-type" }, { - "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.", + "description": "Enables the platform command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-stdin-write" - ] + "const": "os:allow-platform" }, { - "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.", + "description": "Enables the version command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-execute" - ] + "const": "os:allow-version" }, { - "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.", + "description": "Denies the arch command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-kill" - ] + "const": "os:deny-arch" }, { - "description": "shell:deny-open -> Denies the open command without any pre-configured scope.", + "description": "Denies the exe_extension command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-open" - ] + "const": "os:deny-exe-extension" }, { - "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.", + "description": "Denies the family command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-spawn" - ] + "const": "os:deny-family" }, { - "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.", + "description": "Denies the hostname command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-stdin-write" - ] + "const": "os:deny-hostname" }, { - "description": "window-state:default -> This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n", + "description": "Denies the locale command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:default" - ] + "const": "os:deny-locale" }, { - "description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.", + "description": "Denies the os_type command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:allow-filename" - ] + "const": "os:deny-os-type" }, { - "description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.", + "description": "Denies the platform command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:allow-restore-state" - ] + "const": "os:deny-platform" }, { - "description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.", + "description": "Denies the version command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:allow-save-window-state" - ] + "const": "os:deny-version" }, { - "description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.", + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", "type": "string", - "enum": [ - "window-state:deny-filename" - ] + "const": "shell:default" }, { - "description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.", + "description": "Enables the execute command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:deny-restore-state" - ] + "const": "shell:allow-execute" }, { - "description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.", + "description": "Enables the kill command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:deny-save-window-state" - ] + "const": "shell:allow-kill" + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open" + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn" + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write" + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute" + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill" + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open" + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn" + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write" + }, + { + "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n", + "type": "string", + "const": "window-state:default" + }, + { + "description": "Enables the filename command without any pre-configured scope.", + "type": "string", + "const": "window-state:allow-filename" + }, + { + "description": "Enables the restore_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:allow-restore-state" + }, + { + "description": "Enables the save_window_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:allow-save-window-state" + }, + { + "description": "Denies the filename command without any pre-configured scope.", + "type": "string", + "const": "window-state:deny-filename" + }, + { + "description": "Denies the restore_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:deny-restore-state" + }, + { + "description": "Denies the save_window_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:deny-save-window-state" } ] }, @@ -3027,7 +2426,7 @@ } ] }, - "ShellAllowedArg": { + "ShellScopeEntryAllowedArg": { "description": "A command argument allowed to be executed by the webview API.", "anyOf": [ { @@ -3055,18 +2454,18 @@ } ] }, - "ShellAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", "anyOf": [ { "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", "type": "boolean" }, { - "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.", + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", "type": "array", "items": { - "$ref": "#/definitions/ShellAllowedArg" + "$ref": "#/definitions/ShellScopeEntryAllowedArg" } } ] diff --git a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json index 797ccb5c..10fb08fb 100644 --- a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json +++ b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json @@ -133,2803 +133,2202 @@ { "description": "Reference a permission or permission set by identifier and extends its scope.", "type": "object", - "oneOf": [ + "allOf": [ { - "type": "object", - "required": [ - "identifier" - ], + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", + "type": "string", + "const": "shell:default" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute" + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill" + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open" + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn" + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write" + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute" + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill" + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open" + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn" + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write" + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, "properties": { "identifier": { - "oneOf": [ + "description": "Identifier of the permission or permission set.", + "allOf": [ { - "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", - "type": "string", - "enum": [ - "shell:default" - ] - }, + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ { - "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-execute" - ] - }, - { - "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-kill" - ] - }, - { - "description": "shell:allow-open -> Enables the open command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-open" - ] - }, - { - "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-spawn" - ] - }, - { - "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:allow-stdin-write" - ] - }, - { - "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-execute" - ] - }, - { - "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-kill" - ] - }, - { - "description": "shell:deny-open -> Denies the open command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-open" - ] - }, - { - "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-spawn" - ] - }, - { - "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "enum": [ - "shell:deny-stdin-write" - ] + "$ref": "#/definitions/Identifier" } ] }, "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], "items": { - "title": "Entry", - "description": "A command allowed to be executed by the webview API.", - "type": "object", - "required": [ - "args", - "cmd", - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - } + "$ref": "#/definitions/Value" } }, "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], "items": { - "title": "Entry", - "description": "A command allowed to be executed by the webview API.", - "type": "object", - "required": [ - "args", - "cmd", - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - } + "$ref": "#/definitions/Value" } } } } + ], + "required": [ + "identifier" ] } ] }, "Identifier": { + "description": "Permission identifier", "oneOf": [ { - "description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n", + "description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n", "type": "string", - "enum": [ - "clipboard-manager:default" - ] + "const": "clipboard-manager:default" }, { - "description": "clipboard-manager:allow-clear -> Enables the clear command without any pre-configured scope.", + "description": "Enables the clear command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-clear" - ] + "const": "clipboard-manager:allow-clear" }, { - "description": "clipboard-manager:allow-read-image -> Enables the read_image command without any pre-configured scope.", + "description": "Enables the read_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-read-image" - ] + "const": "clipboard-manager:allow-read-image" }, { - "description": "clipboard-manager:allow-read-text -> Enables the read_text command without any pre-configured scope.", + "description": "Enables the read_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-read-text" - ] + "const": "clipboard-manager:allow-read-text" }, { - "description": "clipboard-manager:allow-write-html -> Enables the write_html command without any pre-configured scope.", + "description": "Enables the write_html command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-write-html" - ] + "const": "clipboard-manager:allow-write-html" }, { - "description": "clipboard-manager:allow-write-image -> Enables the write_image command without any pre-configured scope.", + "description": "Enables the write_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-write-image" - ] + "const": "clipboard-manager:allow-write-image" }, { - "description": "clipboard-manager:allow-write-text -> Enables the write_text command without any pre-configured scope.", + "description": "Enables the write_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:allow-write-text" - ] + "const": "clipboard-manager:allow-write-text" }, { - "description": "clipboard-manager:deny-clear -> Denies the clear command without any pre-configured scope.", + "description": "Denies the clear command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-clear" - ] + "const": "clipboard-manager:deny-clear" }, { - "description": "clipboard-manager:deny-read-image -> Denies the read_image command without any pre-configured scope.", + "description": "Denies the read_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-read-image" - ] + "const": "clipboard-manager:deny-read-image" }, { - "description": "clipboard-manager:deny-read-text -> Denies the read_text command without any pre-configured scope.", + "description": "Denies the read_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-read-text" - ] + "const": "clipboard-manager:deny-read-text" }, { - "description": "clipboard-manager:deny-write-html -> Denies the write_html command without any pre-configured scope.", + "description": "Denies the write_html command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-write-html" - ] + "const": "clipboard-manager:deny-write-html" }, { - "description": "clipboard-manager:deny-write-image -> Denies the write_image command without any pre-configured scope.", + "description": "Denies the write_image command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-write-image" - ] + "const": "clipboard-manager:deny-write-image" }, { - "description": "clipboard-manager:deny-write-text -> Denies the write_text command without any pre-configured scope.", + "description": "Denies the write_text command without any pre-configured scope.", "type": "string", - "enum": [ - "clipboard-manager:deny-write-text" - ] + "const": "clipboard-manager:deny-write-text" }, { - "description": "core:app:default -> Default permissions for the plugin.", + "description": "Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n", "type": "string", - "enum": [ - "core:app:default" - ] + "const": "core:default" }, { - "description": "core:app:allow-app-hide -> Enables the app_hide command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:app:allow-app-hide" - ] + "const": "core:app:default" }, { - "description": "core:app:allow-app-show -> Enables the app_show command without any pre-configured scope.", + "description": "Enables the app_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-app-show" - ] + "const": "core:app:allow-app-hide" }, { - "description": "core:app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.", + "description": "Enables the app_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-default-window-icon" - ] + "const": "core:app:allow-app-show" }, { - "description": "core:app:allow-name -> Enables the name command without any pre-configured scope.", + "description": "Enables the default_window_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-name" - ] + "const": "core:app:allow-default-window-icon" }, { - "description": "core:app:allow-tauri-version -> Enables the tauri_version command without any pre-configured scope.", + "description": "Enables the name command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-tauri-version" - ] + "const": "core:app:allow-name" }, { - "description": "core:app:allow-version -> Enables the version command without any pre-configured scope.", + "description": "Enables the set_app_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:allow-version" - ] + "const": "core:app:allow-set-app-theme" }, { - "description": "core:app:deny-app-hide -> Denies the app_hide command without any pre-configured scope.", + "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-app-hide" - ] + "const": "core:app:allow-tauri-version" }, { - "description": "core:app:deny-app-show -> Denies the app_show command without any pre-configured scope.", + "description": "Enables the version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-app-show" - ] + "const": "core:app:allow-version" }, { - "description": "core:app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.", + "description": "Denies the app_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-default-window-icon" - ] + "const": "core:app:deny-app-hide" }, { - "description": "core:app:deny-name -> Denies the name command without any pre-configured scope.", + "description": "Denies the app_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-name" - ] + "const": "core:app:deny-app-show" }, { - "description": "core:app:deny-tauri-version -> Denies the tauri_version command without any pre-configured scope.", + "description": "Denies the default_window_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-tauri-version" - ] + "const": "core:app:deny-default-window-icon" }, { - "description": "core:app:deny-version -> Denies the version command without any pre-configured scope.", + "description": "Denies the name command without any pre-configured scope.", "type": "string", - "enum": [ - "core:app:deny-version" - ] + "const": "core:app:deny-name" }, { - "description": "core:event:default -> Default permissions for the plugin.", + "description": "Denies the set_app_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:default" - ] + "const": "core:app:deny-set-app-theme" }, { - "description": "core:event:allow-emit -> Enables the emit command without any pre-configured scope.", + "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:allow-emit" - ] + "const": "core:app:deny-tauri-version" }, { - "description": "core:event:allow-emit-to -> Enables the emit_to command without any pre-configured scope.", + "description": "Denies the version command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:allow-emit-to" - ] + "const": "core:app:deny-version" }, { - "description": "core:event:allow-listen -> Enables the listen command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:event:allow-listen" - ] + "const": "core:event:default" }, { - "description": "core:event:allow-unlisten -> Enables the unlisten command without any pre-configured scope.", + "description": "Enables the emit command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:allow-unlisten" - ] + "const": "core:event:allow-emit" }, { - "description": "core:event:deny-emit -> Denies the emit command without any pre-configured scope.", + "description": "Enables the emit_to command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-emit" - ] + "const": "core:event:allow-emit-to" }, { - "description": "core:event:deny-emit-to -> Denies the emit_to command without any pre-configured scope.", + "description": "Enables the listen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-emit-to" - ] + "const": "core:event:allow-listen" }, { - "description": "core:event:deny-listen -> Denies the listen command without any pre-configured scope.", + "description": "Enables the unlisten command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-listen" - ] + "const": "core:event:allow-unlisten" }, { - "description": "core:event:deny-unlisten -> Denies the unlisten command without any pre-configured scope.", + "description": "Denies the emit command without any pre-configured scope.", "type": "string", - "enum": [ - "core:event:deny-unlisten" - ] + "const": "core:event:deny-emit" }, { - "description": "core:image:default -> Default permissions for the plugin.", + "description": "Denies the emit_to command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:default" - ] + "const": "core:event:deny-emit-to" }, { - "description": "core:image:allow-from-bytes -> Enables the from_bytes command without any pre-configured scope.", + "description": "Denies the listen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-from-bytes" - ] + "const": "core:event:deny-listen" }, { - "description": "core:image:allow-from-path -> Enables the from_path command without any pre-configured scope.", + "description": "Denies the unlisten command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-from-path" - ] + "const": "core:event:deny-unlisten" }, { - "description": "core:image:allow-new -> Enables the new command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:image:allow-new" - ] + "const": "core:image:default" }, { - "description": "core:image:allow-rgba -> Enables the rgba command without any pre-configured scope.", + "description": "Enables the from_bytes command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-rgba" - ] + "const": "core:image:allow-from-bytes" }, { - "description": "core:image:allow-size -> Enables the size command without any pre-configured scope.", + "description": "Enables the from_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:allow-size" - ] + "const": "core:image:allow-from-path" }, { - "description": "core:image:deny-from-bytes -> Denies the from_bytes command without any pre-configured scope.", + "description": "Enables the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-from-bytes" - ] + "const": "core:image:allow-new" }, { - "description": "core:image:deny-from-path -> Denies the from_path command without any pre-configured scope.", + "description": "Enables the rgba command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-from-path" - ] + "const": "core:image:allow-rgba" }, { - "description": "core:image:deny-new -> Denies the new command without any pre-configured scope.", + "description": "Enables the size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-new" - ] + "const": "core:image:allow-size" }, { - "description": "core:image:deny-rgba -> Denies the rgba command without any pre-configured scope.", + "description": "Denies the from_bytes command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-rgba" - ] + "const": "core:image:deny-from-bytes" }, { - "description": "core:image:deny-size -> Denies the size command without any pre-configured scope.", + "description": "Denies the from_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:image:deny-size" - ] + "const": "core:image:deny-from-path" }, { - "description": "core:menu:default -> Default permissions for the plugin.", + "description": "Denies the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:default" - ] + "const": "core:image:deny-new" }, { - "description": "core:menu:allow-append -> Enables the append command without any pre-configured scope.", + "description": "Denies the rgba command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-append" - ] + "const": "core:image:deny-rgba" }, { - "description": "core:menu:allow-create-default -> Enables the create_default command without any pre-configured scope.", + "description": "Denies the size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-create-default" - ] + "const": "core:image:deny-size" }, { - "description": "core:menu:allow-get -> Enables the get command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:menu:allow-get" - ] + "const": "core:menu:default" }, { - "description": "core:menu:allow-insert -> Enables the insert command without any pre-configured scope.", + "description": "Enables the append command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-insert" - ] + "const": "core:menu:allow-append" }, { - "description": "core:menu:allow-is-checked -> Enables the is_checked command without any pre-configured scope.", + "description": "Enables the create_default command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-is-checked" - ] + "const": "core:menu:allow-create-default" }, { - "description": "core:menu:allow-is-enabled -> Enables the is_enabled command without any pre-configured scope.", + "description": "Enables the get command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-is-enabled" - ] + "const": "core:menu:allow-get" }, { - "description": "core:menu:allow-items -> Enables the items command without any pre-configured scope.", + "description": "Enables the insert command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-items" - ] + "const": "core:menu:allow-insert" }, { - "description": "core:menu:allow-new -> Enables the new command without any pre-configured scope.", + "description": "Enables the is_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-new" - ] + "const": "core:menu:allow-is-checked" }, { - "description": "core:menu:allow-popup -> Enables the popup command without any pre-configured scope.", + "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-popup" - ] + "const": "core:menu:allow-is-enabled" }, { - "description": "core:menu:allow-prepend -> Enables the prepend command without any pre-configured scope.", + "description": "Enables the items command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-prepend" - ] + "const": "core:menu:allow-items" }, { - "description": "core:menu:allow-remove -> Enables the remove command without any pre-configured scope.", + "description": "Enables the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-remove" - ] + "const": "core:menu:allow-new" }, { - "description": "core:menu:allow-remove-at -> Enables the remove_at command without any pre-configured scope.", + "description": "Enables the popup command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-remove-at" - ] + "const": "core:menu:allow-popup" }, { - "description": "core:menu:allow-set-accelerator -> Enables the set_accelerator command without any pre-configured scope.", + "description": "Enables the prepend command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-accelerator" - ] + "const": "core:menu:allow-prepend" }, { - "description": "core:menu:allow-set-as-app-menu -> Enables the set_as_app_menu command without any pre-configured scope.", + "description": "Enables the remove command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-app-menu" - ] + "const": "core:menu:allow-remove" }, { - "description": "core:menu:allow-set-as-help-menu-for-nsapp -> Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "description": "Enables the remove_at command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-help-menu-for-nsapp" - ] + "const": "core:menu:allow-remove-at" }, { - "description": "core:menu:allow-set-as-window-menu -> Enables the set_as_window_menu command without any pre-configured scope.", + "description": "Enables the set_accelerator command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-window-menu" - ] + "const": "core:menu:allow-set-accelerator" }, { - "description": "core:menu:allow-set-as-windows-menu-for-nsapp -> Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "description": "Enables the set_as_app_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-as-windows-menu-for-nsapp" - ] + "const": "core:menu:allow-set-as-app-menu" }, { - "description": "core:menu:allow-set-checked -> Enables the set_checked command without any pre-configured scope.", + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-checked" - ] + "const": "core:menu:allow-set-as-help-menu-for-nsapp" }, { - "description": "core:menu:allow-set-enabled -> Enables the set_enabled command without any pre-configured scope.", + "description": "Enables the set_as_window_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-enabled" - ] + "const": "core:menu:allow-set-as-window-menu" }, { - "description": "core:menu:allow-set-icon -> Enables the set_icon command without any pre-configured scope.", + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-icon" - ] + "const": "core:menu:allow-set-as-windows-menu-for-nsapp" }, { - "description": "core:menu:allow-set-text -> Enables the set_text command without any pre-configured scope.", + "description": "Enables the set_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-set-text" - ] + "const": "core:menu:allow-set-checked" }, { - "description": "core:menu:allow-text -> Enables the text command without any pre-configured scope.", + "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:allow-text" - ] + "const": "core:menu:allow-set-enabled" }, { - "description": "core:menu:deny-append -> Denies the append command without any pre-configured scope.", + "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-append" - ] + "const": "core:menu:allow-set-icon" }, { - "description": "core:menu:deny-create-default -> Denies the create_default command without any pre-configured scope.", + "description": "Enables the set_text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-create-default" - ] + "const": "core:menu:allow-set-text" }, { - "description": "core:menu:deny-get -> Denies the get command without any pre-configured scope.", + "description": "Enables the text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-get" - ] + "const": "core:menu:allow-text" }, { - "description": "core:menu:deny-insert -> Denies the insert command without any pre-configured scope.", + "description": "Denies the append command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-insert" - ] + "const": "core:menu:deny-append" }, { - "description": "core:menu:deny-is-checked -> Denies the is_checked command without any pre-configured scope.", + "description": "Denies the create_default command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-is-checked" - ] + "const": "core:menu:deny-create-default" }, { - "description": "core:menu:deny-is-enabled -> Denies the is_enabled command without any pre-configured scope.", + "description": "Denies the get command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-is-enabled" - ] + "const": "core:menu:deny-get" }, { - "description": "core:menu:deny-items -> Denies the items command without any pre-configured scope.", + "description": "Denies the insert command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-items" - ] + "const": "core:menu:deny-insert" }, { - "description": "core:menu:deny-new -> Denies the new command without any pre-configured scope.", + "description": "Denies the is_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-new" - ] + "const": "core:menu:deny-is-checked" }, { - "description": "core:menu:deny-popup -> Denies the popup command without any pre-configured scope.", + "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-popup" - ] + "const": "core:menu:deny-is-enabled" }, { - "description": "core:menu:deny-prepend -> Denies the prepend command without any pre-configured scope.", + "description": "Denies the items command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-prepend" - ] + "const": "core:menu:deny-items" }, { - "description": "core:menu:deny-remove -> Denies the remove command without any pre-configured scope.", + "description": "Denies the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-remove" - ] + "const": "core:menu:deny-new" }, { - "description": "core:menu:deny-remove-at -> Denies the remove_at command without any pre-configured scope.", + "description": "Denies the popup command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-remove-at" - ] + "const": "core:menu:deny-popup" }, { - "description": "core:menu:deny-set-accelerator -> Denies the set_accelerator command without any pre-configured scope.", + "description": "Denies the prepend command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-accelerator" - ] + "const": "core:menu:deny-prepend" }, { - "description": "core:menu:deny-set-as-app-menu -> Denies the set_as_app_menu command without any pre-configured scope.", + "description": "Denies the remove command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-app-menu" - ] + "const": "core:menu:deny-remove" }, { - "description": "core:menu:deny-set-as-help-menu-for-nsapp -> Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "description": "Denies the remove_at command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-help-menu-for-nsapp" - ] + "const": "core:menu:deny-remove-at" }, { - "description": "core:menu:deny-set-as-window-menu -> Denies the set_as_window_menu command without any pre-configured scope.", + "description": "Denies the set_accelerator command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-window-menu" - ] + "const": "core:menu:deny-set-accelerator" }, { - "description": "core:menu:deny-set-as-windows-menu-for-nsapp -> Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "description": "Denies the set_as_app_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-as-windows-menu-for-nsapp" - ] + "const": "core:menu:deny-set-as-app-menu" }, { - "description": "core:menu:deny-set-checked -> Denies the set_checked command without any pre-configured scope.", + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-checked" - ] + "const": "core:menu:deny-set-as-help-menu-for-nsapp" }, { - "description": "core:menu:deny-set-enabled -> Denies the set_enabled command without any pre-configured scope.", + "description": "Denies the set_as_window_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-enabled" - ] + "const": "core:menu:deny-set-as-window-menu" }, { - "description": "core:menu:deny-set-icon -> Denies the set_icon command without any pre-configured scope.", + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-icon" - ] + "const": "core:menu:deny-set-as-windows-menu-for-nsapp" }, { - "description": "core:menu:deny-set-text -> Denies the set_text command without any pre-configured scope.", + "description": "Denies the set_checked command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-set-text" - ] + "const": "core:menu:deny-set-checked" }, { - "description": "core:menu:deny-text -> Denies the text command without any pre-configured scope.", + "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:menu:deny-text" - ] + "const": "core:menu:deny-set-enabled" }, { - "description": "core:path:default -> Default permissions for the plugin.", + "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:default" - ] + "const": "core:menu:deny-set-icon" }, { - "description": "core:path:allow-basename -> Enables the basename command without any pre-configured scope.", + "description": "Denies the set_text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-basename" - ] + "const": "core:menu:deny-set-text" }, { - "description": "core:path:allow-dirname -> Enables the dirname command without any pre-configured scope.", + "description": "Denies the text command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-dirname" - ] + "const": "core:menu:deny-text" }, { - "description": "core:path:allow-extname -> Enables the extname command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:path:allow-extname" - ] + "const": "core:path:default" }, { - "description": "core:path:allow-is-absolute -> Enables the is_absolute command without any pre-configured scope.", + "description": "Enables the basename command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-is-absolute" - ] + "const": "core:path:allow-basename" }, { - "description": "core:path:allow-join -> Enables the join command without any pre-configured scope.", + "description": "Enables the dirname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-join" - ] + "const": "core:path:allow-dirname" }, { - "description": "core:path:allow-normalize -> Enables the normalize command without any pre-configured scope.", + "description": "Enables the extname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-normalize" - ] + "const": "core:path:allow-extname" }, { - "description": "core:path:allow-resolve -> Enables the resolve command without any pre-configured scope.", + "description": "Enables the is_absolute command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-resolve" - ] + "const": "core:path:allow-is-absolute" }, { - "description": "core:path:allow-resolve-directory -> Enables the resolve_directory command without any pre-configured scope.", + "description": "Enables the join command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:allow-resolve-directory" - ] + "const": "core:path:allow-join" }, { - "description": "core:path:deny-basename -> Denies the basename command without any pre-configured scope.", + "description": "Enables the normalize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-basename" - ] + "const": "core:path:allow-normalize" }, { - "description": "core:path:deny-dirname -> Denies the dirname command without any pre-configured scope.", + "description": "Enables the resolve command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-dirname" - ] + "const": "core:path:allow-resolve" }, { - "description": "core:path:deny-extname -> Denies the extname command without any pre-configured scope.", + "description": "Enables the resolve_directory command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-extname" - ] + "const": "core:path:allow-resolve-directory" }, { - "description": "core:path:deny-is-absolute -> Denies the is_absolute command without any pre-configured scope.", + "description": "Denies the basename command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-is-absolute" - ] + "const": "core:path:deny-basename" }, { - "description": "core:path:deny-join -> Denies the join command without any pre-configured scope.", + "description": "Denies the dirname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-join" - ] + "const": "core:path:deny-dirname" }, { - "description": "core:path:deny-normalize -> Denies the normalize command without any pre-configured scope.", + "description": "Denies the extname command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-normalize" - ] + "const": "core:path:deny-extname" }, { - "description": "core:path:deny-resolve -> Denies the resolve command without any pre-configured scope.", + "description": "Denies the is_absolute command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-resolve" - ] + "const": "core:path:deny-is-absolute" }, { - "description": "core:path:deny-resolve-directory -> Denies the resolve_directory command without any pre-configured scope.", + "description": "Denies the join command without any pre-configured scope.", "type": "string", - "enum": [ - "core:path:deny-resolve-directory" - ] + "const": "core:path:deny-join" }, { - "description": "core:resources:default -> Default permissions for the plugin.", + "description": "Denies the normalize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:resources:default" - ] + "const": "core:path:deny-normalize" }, { - "description": "core:resources:allow-close -> Enables the close command without any pre-configured scope.", + "description": "Denies the resolve command without any pre-configured scope.", "type": "string", - "enum": [ - "core:resources:allow-close" - ] + "const": "core:path:deny-resolve" }, { - "description": "core:resources:deny-close -> Denies the close command without any pre-configured scope.", + "description": "Denies the resolve_directory command without any pre-configured scope.", "type": "string", - "enum": [ - "core:resources:deny-close" - ] + "const": "core:path:deny-resolve-directory" }, { - "description": "core:tray:default -> Default permissions for the plugin.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:tray:default" - ] + "const": "core:resources:default" }, { - "description": "core:tray:allow-get-by-id -> Enables the get_by_id command without any pre-configured scope.", + "description": "Enables the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-get-by-id" - ] + "const": "core:resources:allow-close" }, { - "description": "core:tray:allow-new -> Enables the new command without any pre-configured scope.", + "description": "Denies the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-new" - ] + "const": "core:resources:deny-close" }, { - "description": "core:tray:allow-remove-by-id -> Enables the remove_by_id command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:tray:allow-remove-by-id" - ] + "const": "core:tray:default" }, { - "description": "core:tray:allow-set-icon -> Enables the set_icon command without any pre-configured scope.", + "description": "Enables the get_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-icon" - ] + "const": "core:tray:allow-get-by-id" }, { - "description": "core:tray:allow-set-icon-as-template -> Enables the set_icon_as_template command without any pre-configured scope.", + "description": "Enables the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-icon-as-template" - ] + "const": "core:tray:allow-new" }, { - "description": "core:tray:allow-set-menu -> Enables the set_menu command without any pre-configured scope.", + "description": "Enables the remove_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-menu" - ] + "const": "core:tray:allow-remove-by-id" }, { - "description": "core:tray:allow-set-show-menu-on-left-click -> Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-show-menu-on-left-click" - ] + "const": "core:tray:allow-set-icon" }, { - "description": "core:tray:allow-set-temp-dir-path -> Enables the set_temp_dir_path command without any pre-configured scope.", + "description": "Enables the set_icon_as_template command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-temp-dir-path" - ] + "const": "core:tray:allow-set-icon-as-template" }, { - "description": "core:tray:allow-set-title -> Enables the set_title command without any pre-configured scope.", + "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-title" - ] + "const": "core:tray:allow-set-menu" }, { - "description": "core:tray:allow-set-tooltip -> Enables the set_tooltip command without any pre-configured scope.", + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-tooltip" - ] + "const": "core:tray:allow-set-show-menu-on-left-click" }, { - "description": "core:tray:allow-set-visible -> Enables the set_visible command without any pre-configured scope.", + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:allow-set-visible" - ] + "const": "core:tray:allow-set-temp-dir-path" }, { - "description": "core:tray:deny-get-by-id -> Denies the get_by_id command without any pre-configured scope.", + "description": "Enables the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-get-by-id" - ] + "const": "core:tray:allow-set-title" }, { - "description": "core:tray:deny-new -> Denies the new command without any pre-configured scope.", + "description": "Enables the set_tooltip command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-new" - ] + "const": "core:tray:allow-set-tooltip" }, { - "description": "core:tray:deny-remove-by-id -> Denies the remove_by_id command without any pre-configured scope.", + "description": "Enables the set_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-remove-by-id" - ] + "const": "core:tray:allow-set-visible" }, { - "description": "core:tray:deny-set-icon -> Denies the set_icon command without any pre-configured scope.", + "description": "Denies the get_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-icon" - ] + "const": "core:tray:deny-get-by-id" }, { - "description": "core:tray:deny-set-icon-as-template -> Denies the set_icon_as_template command without any pre-configured scope.", + "description": "Denies the new command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-icon-as-template" - ] + "const": "core:tray:deny-new" }, { - "description": "core:tray:deny-set-menu -> Denies the set_menu command without any pre-configured scope.", + "description": "Denies the remove_by_id command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-menu" - ] + "const": "core:tray:deny-remove-by-id" }, { - "description": "core:tray:deny-set-show-menu-on-left-click -> Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-show-menu-on-left-click" - ] + "const": "core:tray:deny-set-icon" }, { - "description": "core:tray:deny-set-temp-dir-path -> Denies the set_temp_dir_path command without any pre-configured scope.", + "description": "Denies the set_icon_as_template command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-temp-dir-path" - ] + "const": "core:tray:deny-set-icon-as-template" }, { - "description": "core:tray:deny-set-title -> Denies the set_title command without any pre-configured scope.", + "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-title" - ] + "const": "core:tray:deny-set-menu" }, { - "description": "core:tray:deny-set-tooltip -> Denies the set_tooltip command without any pre-configured scope.", + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-tooltip" - ] + "const": "core:tray:deny-set-show-menu-on-left-click" }, { - "description": "core:tray:deny-set-visible -> Denies the set_visible command without any pre-configured scope.", + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", "type": "string", - "enum": [ - "core:tray:deny-set-visible" - ] + "const": "core:tray:deny-set-temp-dir-path" }, { - "description": "core:webview:default -> Default permissions for the plugin.", + "description": "Denies the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:default" - ] + "const": "core:tray:deny-set-title" }, { - "description": "core:webview:allow-create-webview -> Enables the create_webview command without any pre-configured scope.", + "description": "Denies the set_tooltip command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-create-webview" - ] + "const": "core:tray:deny-set-tooltip" }, { - "description": "core:webview:allow-create-webview-window -> Enables the create_webview_window command without any pre-configured scope.", + "description": "Denies the set_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-create-webview-window" - ] + "const": "core:tray:deny-set-visible" }, { - "description": "core:webview:allow-get-all-webviews -> Enables the get_all_webviews command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:webview:allow-get-all-webviews" - ] + "const": "core:webview:default" }, { - "description": "core:webview:allow-internal-toggle-devtools -> Enables the internal_toggle_devtools command without any pre-configured scope.", + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-internal-toggle-devtools" - ] + "const": "core:webview:allow-clear-all-browsing-data" }, { - "description": "core:webview:allow-print -> Enables the print command without any pre-configured scope.", + "description": "Enables the create_webview command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-print" - ] + "const": "core:webview:allow-create-webview" }, { - "description": "core:webview:allow-reparent -> Enables the reparent command without any pre-configured scope.", + "description": "Enables the create_webview_window command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-reparent" - ] + "const": "core:webview:allow-create-webview-window" }, { - "description": "core:webview:allow-set-webview-focus -> Enables the set_webview_focus command without any pre-configured scope.", + "description": "Enables the get_all_webviews command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-focus" - ] + "const": "core:webview:allow-get-all-webviews" }, { - "description": "core:webview:allow-set-webview-position -> Enables the set_webview_position command without any pre-configured scope.", + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-position" - ] + "const": "core:webview:allow-internal-toggle-devtools" }, { - "description": "core:webview:allow-set-webview-size -> Enables the set_webview_size command without any pre-configured scope.", + "description": "Enables the print command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-size" - ] + "const": "core:webview:allow-print" }, { - "description": "core:webview:allow-set-webview-zoom -> Enables the set_webview_zoom command without any pre-configured scope.", + "description": "Enables the reparent command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-set-webview-zoom" - ] + "const": "core:webview:allow-reparent" }, { - "description": "core:webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.", + "description": "Enables the set_webview_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-webview-close" - ] + "const": "core:webview:allow-set-webview-focus" }, { - "description": "core:webview:allow-webview-position -> Enables the webview_position command without any pre-configured scope.", + "description": "Enables the set_webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-webview-position" - ] + "const": "core:webview:allow-set-webview-position" }, { - "description": "core:webview:allow-webview-size -> Enables the webview_size command without any pre-configured scope.", + "description": "Enables the set_webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:allow-webview-size" - ] + "const": "core:webview:allow-set-webview-size" }, { - "description": "core:webview:deny-create-webview -> Denies the create_webview command without any pre-configured scope.", + "description": "Enables the set_webview_zoom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-create-webview" - ] + "const": "core:webview:allow-set-webview-zoom" }, { - "description": "core:webview:deny-create-webview-window -> Denies the create_webview_window command without any pre-configured scope.", + "description": "Enables the webview_close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-create-webview-window" - ] + "const": "core:webview:allow-webview-close" }, { - "description": "core:webview:deny-get-all-webviews -> Denies the get_all_webviews command without any pre-configured scope.", + "description": "Enables the webview_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-get-all-webviews" - ] + "const": "core:webview:allow-webview-hide" }, { - "description": "core:webview:deny-internal-toggle-devtools -> Denies the internal_toggle_devtools command without any pre-configured scope.", + "description": "Enables the webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-internal-toggle-devtools" - ] + "const": "core:webview:allow-webview-position" }, { - "description": "core:webview:deny-print -> Denies the print command without any pre-configured scope.", + "description": "Enables the webview_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-print" - ] + "const": "core:webview:allow-webview-show" }, { - "description": "core:webview:deny-reparent -> Denies the reparent command without any pre-configured scope.", + "description": "Enables the webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-reparent" - ] + "const": "core:webview:allow-webview-size" }, { - "description": "core:webview:deny-set-webview-focus -> Denies the set_webview_focus command without any pre-configured scope.", + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-focus" - ] + "const": "core:webview:deny-clear-all-browsing-data" }, { - "description": "core:webview:deny-set-webview-position -> Denies the set_webview_position command without any pre-configured scope.", + "description": "Denies the create_webview command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-position" - ] + "const": "core:webview:deny-create-webview" }, { - "description": "core:webview:deny-set-webview-size -> Denies the set_webview_size command without any pre-configured scope.", + "description": "Denies the create_webview_window command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-size" - ] + "const": "core:webview:deny-create-webview-window" }, { - "description": "core:webview:deny-set-webview-zoom -> Denies the set_webview_zoom command without any pre-configured scope.", + "description": "Denies the get_all_webviews command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-set-webview-zoom" - ] + "const": "core:webview:deny-get-all-webviews" }, { - "description": "core:webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.", + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-webview-close" - ] + "const": "core:webview:deny-internal-toggle-devtools" }, { - "description": "core:webview:deny-webview-position -> Denies the webview_position command without any pre-configured scope.", + "description": "Denies the print command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-webview-position" - ] + "const": "core:webview:deny-print" }, { - "description": "core:webview:deny-webview-size -> Denies the webview_size command without any pre-configured scope.", + "description": "Denies the reparent command without any pre-configured scope.", "type": "string", - "enum": [ - "core:webview:deny-webview-size" - ] + "const": "core:webview:deny-reparent" }, { - "description": "core:window:default -> Default permissions for the plugin.", + "description": "Denies the set_webview_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:default" - ] + "const": "core:webview:deny-set-webview-focus" }, { - "description": "core:window:allow-available-monitors -> Enables the available_monitors command without any pre-configured scope.", + "description": "Denies the set_webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-available-monitors" - ] + "const": "core:webview:deny-set-webview-position" }, { - "description": "core:window:allow-center -> Enables the center command without any pre-configured scope.", + "description": "Denies the set_webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-center" - ] + "const": "core:webview:deny-set-webview-size" }, { - "description": "core:window:allow-close -> Enables the close command without any pre-configured scope.", + "description": "Denies the set_webview_zoom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-close" - ] + "const": "core:webview:deny-set-webview-zoom" }, { - "description": "core:window:allow-create -> Enables the create command without any pre-configured scope.", + "description": "Denies the webview_close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-create" - ] + "const": "core:webview:deny-webview-close" }, { - "description": "core:window:allow-current-monitor -> Enables the current_monitor command without any pre-configured scope.", + "description": "Denies the webview_hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-current-monitor" - ] + "const": "core:webview:deny-webview-hide" }, { - "description": "core:window:allow-cursor-position -> Enables the cursor_position command without any pre-configured scope.", + "description": "Denies the webview_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-cursor-position" - ] + "const": "core:webview:deny-webview-position" }, { - "description": "core:window:allow-destroy -> Enables the destroy command without any pre-configured scope.", + "description": "Denies the webview_show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-destroy" - ] + "const": "core:webview:deny-webview-show" }, { - "description": "core:window:allow-get-all-windows -> Enables the get_all_windows command without any pre-configured scope.", + "description": "Denies the webview_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-get-all-windows" - ] + "const": "core:webview:deny-webview-size" }, { - "description": "core:window:allow-hide -> Enables the hide command without any pre-configured scope.", + "description": "Default permissions for the plugin.", "type": "string", - "enum": [ - "core:window:allow-hide" - ] + "const": "core:window:default" }, { - "description": "core:window:allow-inner-position -> Enables the inner_position command without any pre-configured scope.", + "description": "Enables the available_monitors command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-inner-position" - ] + "const": "core:window:allow-available-monitors" }, { - "description": "core:window:allow-inner-size -> Enables the inner_size command without any pre-configured scope.", + "description": "Enables the center command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-inner-size" - ] + "const": "core:window:allow-center" }, { - "description": "core:window:allow-internal-toggle-maximize -> Enables the internal_toggle_maximize command without any pre-configured scope.", + "description": "Enables the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-internal-toggle-maximize" - ] + "const": "core:window:allow-close" }, { - "description": "core:window:allow-is-closable -> Enables the is_closable command without any pre-configured scope.", + "description": "Enables the create command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-closable" - ] + "const": "core:window:allow-create" }, { - "description": "core:window:allow-is-decorated -> Enables the is_decorated command without any pre-configured scope.", + "description": "Enables the current_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-decorated" - ] + "const": "core:window:allow-current-monitor" }, { - "description": "core:window:allow-is-focused -> Enables the is_focused command without any pre-configured scope.", + "description": "Enables the cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-focused" - ] + "const": "core:window:allow-cursor-position" }, { - "description": "core:window:allow-is-fullscreen -> Enables the is_fullscreen command without any pre-configured scope.", + "description": "Enables the destroy command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-fullscreen" - ] + "const": "core:window:allow-destroy" }, { - "description": "core:window:allow-is-maximizable -> Enables the is_maximizable command without any pre-configured scope.", + "description": "Enables the get_all_windows command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-maximizable" - ] + "const": "core:window:allow-get-all-windows" }, { - "description": "core:window:allow-is-maximized -> Enables the is_maximized command without any pre-configured scope.", + "description": "Enables the hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-maximized" - ] + "const": "core:window:allow-hide" }, { - "description": "core:window:allow-is-minimizable -> Enables the is_minimizable command without any pre-configured scope.", + "description": "Enables the inner_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-minimizable" - ] + "const": "core:window:allow-inner-position" }, { - "description": "core:window:allow-is-minimized -> Enables the is_minimized command without any pre-configured scope.", + "description": "Enables the inner_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-minimized" - ] + "const": "core:window:allow-inner-size" }, { - "description": "core:window:allow-is-resizable -> Enables the is_resizable command without any pre-configured scope.", + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-resizable" - ] + "const": "core:window:allow-internal-toggle-maximize" }, { - "description": "core:window:allow-is-visible -> Enables the is_visible command without any pre-configured scope.", + "description": "Enables the is_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-is-visible" - ] + "const": "core:window:allow-is-closable" }, { - "description": "core:window:allow-maximize -> Enables the maximize command without any pre-configured scope.", + "description": "Enables the is_decorated command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-maximize" - ] + "const": "core:window:allow-is-decorated" }, { - "description": "core:window:allow-minimize -> Enables the minimize command without any pre-configured scope.", + "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-minimize" - ] + "const": "core:window:allow-is-enabled" }, { - "description": "core:window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.", + "description": "Enables the is_focused command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-monitor-from-point" - ] + "const": "core:window:allow-is-focused" }, { - "description": "core:window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", + "description": "Enables the is_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-outer-position" - ] + "const": "core:window:allow-is-fullscreen" }, { - "description": "core:window:allow-outer-size -> Enables the outer_size command without any pre-configured scope.", + "description": "Enables the is_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-outer-size" - ] + "const": "core:window:allow-is-maximizable" }, { - "description": "core:window:allow-primary-monitor -> Enables the primary_monitor command without any pre-configured scope.", + "description": "Enables the is_maximized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-primary-monitor" - ] + "const": "core:window:allow-is-maximized" }, { - "description": "core:window:allow-request-user-attention -> Enables the request_user_attention command without any pre-configured scope.", + "description": "Enables the is_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-request-user-attention" - ] + "const": "core:window:allow-is-minimizable" }, { - "description": "core:window:allow-scale-factor -> Enables the scale_factor command without any pre-configured scope.", + "description": "Enables the is_minimized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-scale-factor" - ] + "const": "core:window:allow-is-minimized" }, { - "description": "core:window:allow-set-always-on-bottom -> Enables the set_always_on_bottom command without any pre-configured scope.", + "description": "Enables the is_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-always-on-bottom" - ] + "const": "core:window:allow-is-resizable" }, { - "description": "core:window:allow-set-always-on-top -> Enables the set_always_on_top command without any pre-configured scope.", + "description": "Enables the is_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-always-on-top" - ] + "const": "core:window:allow-is-visible" }, { - "description": "core:window:allow-set-closable -> Enables the set_closable command without any pre-configured scope.", + "description": "Enables the maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-closable" - ] + "const": "core:window:allow-maximize" }, { - "description": "core:window:allow-set-content-protected -> Enables the set_content_protected command without any pre-configured scope.", + "description": "Enables the minimize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-content-protected" - ] + "const": "core:window:allow-minimize" }, { - "description": "core:window:allow-set-cursor-grab -> Enables the set_cursor_grab command without any pre-configured scope.", + "description": "Enables the monitor_from_point command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-grab" - ] + "const": "core:window:allow-monitor-from-point" }, { - "description": "core:window:allow-set-cursor-icon -> Enables the set_cursor_icon command without any pre-configured scope.", + "description": "Enables the outer_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-icon" - ] + "const": "core:window:allow-outer-position" }, { - "description": "core:window:allow-set-cursor-position -> Enables the set_cursor_position command without any pre-configured scope.", + "description": "Enables the outer_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-position" - ] + "const": "core:window:allow-outer-size" }, { - "description": "core:window:allow-set-cursor-visible -> Enables the set_cursor_visible command without any pre-configured scope.", + "description": "Enables the primary_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-cursor-visible" - ] + "const": "core:window:allow-primary-monitor" }, { - "description": "core:window:allow-set-decorations -> Enables the set_decorations command without any pre-configured scope.", + "description": "Enables the request_user_attention command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-decorations" - ] + "const": "core:window:allow-request-user-attention" }, { - "description": "core:window:allow-set-effects -> Enables the set_effects command without any pre-configured scope.", + "description": "Enables the scale_factor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-effects" - ] + "const": "core:window:allow-scale-factor" }, { - "description": "core:window:allow-set-focus -> Enables the set_focus command without any pre-configured scope.", + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-focus" - ] + "const": "core:window:allow-set-always-on-bottom" }, { - "description": "core:window:allow-set-fullscreen -> Enables the set_fullscreen command without any pre-configured scope.", + "description": "Enables the set_always_on_top command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-fullscreen" - ] + "const": "core:window:allow-set-always-on-top" }, { - "description": "core:window:allow-set-icon -> Enables the set_icon command without any pre-configured scope.", + "description": "Enables the set_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-icon" - ] + "const": "core:window:allow-set-closable" }, { - "description": "core:window:allow-set-ignore-cursor-events -> Enables the set_ignore_cursor_events command without any pre-configured scope.", + "description": "Enables the set_content_protected command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-ignore-cursor-events" - ] + "const": "core:window:allow-set-content-protected" }, { - "description": "core:window:allow-set-max-size -> Enables the set_max_size command without any pre-configured scope.", + "description": "Enables the set_cursor_grab command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-max-size" - ] + "const": "core:window:allow-set-cursor-grab" }, { - "description": "core:window:allow-set-maximizable -> Enables the set_maximizable command without any pre-configured scope.", + "description": "Enables the set_cursor_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-maximizable" - ] + "const": "core:window:allow-set-cursor-icon" }, { - "description": "core:window:allow-set-min-size -> Enables the set_min_size command without any pre-configured scope.", + "description": "Enables the set_cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-min-size" - ] + "const": "core:window:allow-set-cursor-position" }, { - "description": "core:window:allow-set-minimizable -> Enables the set_minimizable command without any pre-configured scope.", + "description": "Enables the set_cursor_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-minimizable" - ] + "const": "core:window:allow-set-cursor-visible" }, { - "description": "core:window:allow-set-position -> Enables the set_position command without any pre-configured scope.", + "description": "Enables the set_decorations command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-position" - ] + "const": "core:window:allow-set-decorations" }, { - "description": "core:window:allow-set-progress-bar -> Enables the set_progress_bar command without any pre-configured scope.", + "description": "Enables the set_effects command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-progress-bar" - ] + "const": "core:window:allow-set-effects" }, { - "description": "core:window:allow-set-resizable -> Enables the set_resizable command without any pre-configured scope.", + "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-resizable" - ] + "const": "core:window:allow-set-enabled" }, { - "description": "core:window:allow-set-shadow -> Enables the set_shadow command without any pre-configured scope.", + "description": "Enables the set_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-shadow" - ] + "const": "core:window:allow-set-focus" }, { - "description": "core:window:allow-set-size -> Enables the set_size command without any pre-configured scope.", + "description": "Enables the set_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-size" - ] + "const": "core:window:allow-set-fullscreen" }, { - "description": "core:window:allow-set-size-constraints -> Enables the set_size_constraints command without any pre-configured scope.", + "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-size-constraints" - ] + "const": "core:window:allow-set-icon" }, { - "description": "core:window:allow-set-skip-taskbar -> Enables the set_skip_taskbar command without any pre-configured scope.", + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-skip-taskbar" - ] + "const": "core:window:allow-set-ignore-cursor-events" }, { - "description": "core:window:allow-set-title -> Enables the set_title command without any pre-configured scope.", + "description": "Enables the set_max_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-title" - ] + "const": "core:window:allow-set-max-size" }, { - "description": "core:window:allow-set-title-bar-style -> Enables the set_title_bar_style command without any pre-configured scope.", + "description": "Enables the set_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-title-bar-style" - ] + "const": "core:window:allow-set-maximizable" }, { - "description": "core:window:allow-set-visible-on-all-workspaces -> Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "description": "Enables the set_min_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-set-visible-on-all-workspaces" - ] + "const": "core:window:allow-set-min-size" }, { - "description": "core:window:allow-show -> Enables the show command without any pre-configured scope.", + "description": "Enables the set_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-show" - ] + "const": "core:window:allow-set-minimizable" }, { - "description": "core:window:allow-start-dragging -> Enables the start_dragging command without any pre-configured scope.", + "description": "Enables the set_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-start-dragging" - ] + "const": "core:window:allow-set-position" }, { - "description": "core:window:allow-start-resize-dragging -> Enables the start_resize_dragging command without any pre-configured scope.", + "description": "Enables the set_progress_bar command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-start-resize-dragging" - ] + "const": "core:window:allow-set-progress-bar" }, { - "description": "core:window:allow-theme -> Enables the theme command without any pre-configured scope.", + "description": "Enables the set_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-theme" - ] + "const": "core:window:allow-set-resizable" }, { - "description": "core:window:allow-title -> Enables the title command without any pre-configured scope.", + "description": "Enables the set_shadow command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-title" - ] + "const": "core:window:allow-set-shadow" }, { - "description": "core:window:allow-toggle-maximize -> Enables the toggle_maximize command without any pre-configured scope.", + "description": "Enables the set_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-toggle-maximize" - ] + "const": "core:window:allow-set-size" }, { - "description": "core:window:allow-unmaximize -> Enables the unmaximize command without any pre-configured scope.", + "description": "Enables the set_size_constraints command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-unmaximize" - ] + "const": "core:window:allow-set-size-constraints" }, { - "description": "core:window:allow-unminimize -> Enables the unminimize command without any pre-configured scope.", + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:allow-unminimize" - ] + "const": "core:window:allow-set-skip-taskbar" }, { - "description": "core:window:deny-available-monitors -> Denies the available_monitors command without any pre-configured scope.", + "description": "Enables the set_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-available-monitors" - ] + "const": "core:window:allow-set-theme" }, { - "description": "core:window:deny-center -> Denies the center command without any pre-configured scope.", + "description": "Enables the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-center" - ] + "const": "core:window:allow-set-title" }, { - "description": "core:window:deny-close -> Denies the close command without any pre-configured scope.", + "description": "Enables the set_title_bar_style command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-close" - ] + "const": "core:window:allow-set-title-bar-style" }, { - "description": "core:window:deny-create -> Denies the create command without any pre-configured scope.", + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-create" - ] + "const": "core:window:allow-set-visible-on-all-workspaces" }, { - "description": "core:window:deny-current-monitor -> Denies the current_monitor command without any pre-configured scope.", + "description": "Enables the show command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-current-monitor" - ] + "const": "core:window:allow-show" }, { - "description": "core:window:deny-cursor-position -> Denies the cursor_position command without any pre-configured scope.", + "description": "Enables the start_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-cursor-position" - ] + "const": "core:window:allow-start-dragging" }, { - "description": "core:window:deny-destroy -> Denies the destroy command without any pre-configured scope.", + "description": "Enables the start_resize_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-destroy" - ] + "const": "core:window:allow-start-resize-dragging" }, { - "description": "core:window:deny-get-all-windows -> Denies the get_all_windows command without any pre-configured scope.", + "description": "Enables the theme command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-get-all-windows" - ] + "const": "core:window:allow-theme" }, { - "description": "core:window:deny-hide -> Denies the hide command without any pre-configured scope.", + "description": "Enables the title command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-hide" - ] + "const": "core:window:allow-title" }, { - "description": "core:window:deny-inner-position -> Denies the inner_position command without any pre-configured scope.", + "description": "Enables the toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-inner-position" - ] + "const": "core:window:allow-toggle-maximize" }, { - "description": "core:window:deny-inner-size -> Denies the inner_size command without any pre-configured scope.", + "description": "Enables the unmaximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-inner-size" - ] + "const": "core:window:allow-unmaximize" }, { - "description": "core:window:deny-internal-toggle-maximize -> Denies the internal_toggle_maximize command without any pre-configured scope.", + "description": "Enables the unminimize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-internal-toggle-maximize" - ] + "const": "core:window:allow-unminimize" }, { - "description": "core:window:deny-is-closable -> Denies the is_closable command without any pre-configured scope.", + "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-closable" - ] + "const": "core:window:deny-available-monitors" }, { - "description": "core:window:deny-is-decorated -> Denies the is_decorated command without any pre-configured scope.", + "description": "Denies the center command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-decorated" - ] + "const": "core:window:deny-center" }, { - "description": "core:window:deny-is-focused -> Denies the is_focused command without any pre-configured scope.", + "description": "Denies the close command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-focused" - ] + "const": "core:window:deny-close" }, { - "description": "core:window:deny-is-fullscreen -> Denies the is_fullscreen command without any pre-configured scope.", + "description": "Denies the create command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-fullscreen" - ] + "const": "core:window:deny-create" }, { - "description": "core:window:deny-is-maximizable -> Denies the is_maximizable command without any pre-configured scope.", + "description": "Denies the current_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-maximizable" - ] + "const": "core:window:deny-current-monitor" }, { - "description": "core:window:deny-is-maximized -> Denies the is_maximized command without any pre-configured scope.", + "description": "Denies the cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-maximized" - ] + "const": "core:window:deny-cursor-position" }, { - "description": "core:window:deny-is-minimizable -> Denies the is_minimizable command without any pre-configured scope.", + "description": "Denies the destroy command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-minimizable" - ] + "const": "core:window:deny-destroy" }, { - "description": "core:window:deny-is-minimized -> Denies the is_minimized command without any pre-configured scope.", + "description": "Denies the get_all_windows command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-minimized" - ] + "const": "core:window:deny-get-all-windows" }, { - "description": "core:window:deny-is-resizable -> Denies the is_resizable command without any pre-configured scope.", + "description": "Denies the hide command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-resizable" - ] + "const": "core:window:deny-hide" }, { - "description": "core:window:deny-is-visible -> Denies the is_visible command without any pre-configured scope.", + "description": "Denies the inner_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-is-visible" - ] + "const": "core:window:deny-inner-position" }, { - "description": "core:window:deny-maximize -> Denies the maximize command without any pre-configured scope.", + "description": "Denies the inner_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-maximize" - ] + "const": "core:window:deny-inner-size" }, { - "description": "core:window:deny-minimize -> Denies the minimize command without any pre-configured scope.", + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-minimize" - ] + "const": "core:window:deny-internal-toggle-maximize" }, { - "description": "core:window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.", + "description": "Denies the is_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-monitor-from-point" - ] + "const": "core:window:deny-is-closable" }, { - "description": "core:window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", + "description": "Denies the is_decorated command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-outer-position" - ] + "const": "core:window:deny-is-decorated" }, { - "description": "core:window:deny-outer-size -> Denies the outer_size command without any pre-configured scope.", + "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-outer-size" - ] + "const": "core:window:deny-is-enabled" }, { - "description": "core:window:deny-primary-monitor -> Denies the primary_monitor command without any pre-configured scope.", + "description": "Denies the is_focused command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-primary-monitor" - ] + "const": "core:window:deny-is-focused" }, { - "description": "core:window:deny-request-user-attention -> Denies the request_user_attention command without any pre-configured scope.", + "description": "Denies the is_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-request-user-attention" - ] + "const": "core:window:deny-is-fullscreen" }, { - "description": "core:window:deny-scale-factor -> Denies the scale_factor command without any pre-configured scope.", + "description": "Denies the is_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-scale-factor" - ] + "const": "core:window:deny-is-maximizable" }, { - "description": "core:window:deny-set-always-on-bottom -> Denies the set_always_on_bottom command without any pre-configured scope.", + "description": "Denies the is_maximized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-always-on-bottom" - ] + "const": "core:window:deny-is-maximized" }, { - "description": "core:window:deny-set-always-on-top -> Denies the set_always_on_top command without any pre-configured scope.", + "description": "Denies the is_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-always-on-top" - ] + "const": "core:window:deny-is-minimizable" }, { - "description": "core:window:deny-set-closable -> Denies the set_closable command without any pre-configured scope.", + "description": "Denies the is_minimized command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-closable" - ] + "const": "core:window:deny-is-minimized" }, { - "description": "core:window:deny-set-content-protected -> Denies the set_content_protected command without any pre-configured scope.", + "description": "Denies the is_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-content-protected" - ] + "const": "core:window:deny-is-resizable" }, { - "description": "core:window:deny-set-cursor-grab -> Denies the set_cursor_grab command without any pre-configured scope.", + "description": "Denies the is_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-grab" - ] + "const": "core:window:deny-is-visible" }, { - "description": "core:window:deny-set-cursor-icon -> Denies the set_cursor_icon command without any pre-configured scope.", + "description": "Denies the maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-icon" - ] + "const": "core:window:deny-maximize" }, { - "description": "core:window:deny-set-cursor-position -> Denies the set_cursor_position command without any pre-configured scope.", + "description": "Denies the minimize command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-position" - ] + "const": "core:window:deny-minimize" }, { - "description": "core:window:deny-set-cursor-visible -> Denies the set_cursor_visible command without any pre-configured scope.", + "description": "Denies the monitor_from_point command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-cursor-visible" - ] + "const": "core:window:deny-monitor-from-point" }, { - "description": "core:window:deny-set-decorations -> Denies the set_decorations command without any pre-configured scope.", + "description": "Denies the outer_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-decorations" - ] + "const": "core:window:deny-outer-position" }, { - "description": "core:window:deny-set-effects -> Denies the set_effects command without any pre-configured scope.", + "description": "Denies the outer_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-effects" - ] + "const": "core:window:deny-outer-size" }, { - "description": "core:window:deny-set-focus -> Denies the set_focus command without any pre-configured scope.", + "description": "Denies the primary_monitor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-focus" - ] + "const": "core:window:deny-primary-monitor" }, { - "description": "core:window:deny-set-fullscreen -> Denies the set_fullscreen command without any pre-configured scope.", + "description": "Denies the request_user_attention command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-fullscreen" - ] + "const": "core:window:deny-request-user-attention" }, { - "description": "core:window:deny-set-icon -> Denies the set_icon command without any pre-configured scope.", + "description": "Denies the scale_factor command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-icon" - ] + "const": "core:window:deny-scale-factor" }, { - "description": "core:window:deny-set-ignore-cursor-events -> Denies the set_ignore_cursor_events command without any pre-configured scope.", + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-ignore-cursor-events" - ] + "const": "core:window:deny-set-always-on-bottom" }, { - "description": "core:window:deny-set-max-size -> Denies the set_max_size command without any pre-configured scope.", + "description": "Denies the set_always_on_top command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-max-size" - ] + "const": "core:window:deny-set-always-on-top" }, { - "description": "core:window:deny-set-maximizable -> Denies the set_maximizable command without any pre-configured scope.", + "description": "Denies the set_closable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-maximizable" - ] + "const": "core:window:deny-set-closable" }, { - "description": "core:window:deny-set-min-size -> Denies the set_min_size command without any pre-configured scope.", + "description": "Denies the set_content_protected command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-min-size" - ] + "const": "core:window:deny-set-content-protected" }, { - "description": "core:window:deny-set-minimizable -> Denies the set_minimizable command without any pre-configured scope.", + "description": "Denies the set_cursor_grab command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-minimizable" - ] + "const": "core:window:deny-set-cursor-grab" }, { - "description": "core:window:deny-set-position -> Denies the set_position command without any pre-configured scope.", + "description": "Denies the set_cursor_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-position" - ] + "const": "core:window:deny-set-cursor-icon" }, { - "description": "core:window:deny-set-progress-bar -> Denies the set_progress_bar command without any pre-configured scope.", + "description": "Denies the set_cursor_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-progress-bar" - ] + "const": "core:window:deny-set-cursor-position" }, { - "description": "core:window:deny-set-resizable -> Denies the set_resizable command without any pre-configured scope.", + "description": "Denies the set_cursor_visible command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-resizable" - ] + "const": "core:window:deny-set-cursor-visible" }, { - "description": "core:window:deny-set-shadow -> Denies the set_shadow command without any pre-configured scope.", + "description": "Denies the set_decorations command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-shadow" - ] + "const": "core:window:deny-set-decorations" }, { - "description": "core:window:deny-set-size -> Denies the set_size command without any pre-configured scope.", + "description": "Denies the set_effects command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-size" - ] + "const": "core:window:deny-set-effects" }, { - "description": "core:window:deny-set-size-constraints -> Denies the set_size_constraints command without any pre-configured scope.", + "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-size-constraints" - ] + "const": "core:window:deny-set-enabled" }, { - "description": "core:window:deny-set-skip-taskbar -> Denies the set_skip_taskbar command without any pre-configured scope.", + "description": "Denies the set_focus command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-skip-taskbar" - ] + "const": "core:window:deny-set-focus" }, { - "description": "core:window:deny-set-title -> Denies the set_title command without any pre-configured scope.", + "description": "Denies the set_fullscreen command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-title" - ] + "const": "core:window:deny-set-fullscreen" }, { - "description": "core:window:deny-set-title-bar-style -> Denies the set_title_bar_style command without any pre-configured scope.", + "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-title-bar-style" - ] + "const": "core:window:deny-set-icon" }, { - "description": "core:window:deny-set-visible-on-all-workspaces -> Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-set-visible-on-all-workspaces" - ] + "const": "core:window:deny-set-ignore-cursor-events" }, { - "description": "core:window:deny-show -> Denies the show command without any pre-configured scope.", + "description": "Denies the set_max_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-show" - ] + "const": "core:window:deny-set-max-size" }, { - "description": "core:window:deny-start-dragging -> Denies the start_dragging command without any pre-configured scope.", + "description": "Denies the set_maximizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-start-dragging" - ] + "const": "core:window:deny-set-maximizable" }, { - "description": "core:window:deny-start-resize-dragging -> Denies the start_resize_dragging command without any pre-configured scope.", + "description": "Denies the set_min_size command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-start-resize-dragging" - ] + "const": "core:window:deny-set-min-size" }, { - "description": "core:window:deny-theme -> Denies the theme command without any pre-configured scope.", + "description": "Denies the set_minimizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-theme" - ] + "const": "core:window:deny-set-minimizable" }, { - "description": "core:window:deny-title -> Denies the title command without any pre-configured scope.", + "description": "Denies the set_position command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-title" - ] + "const": "core:window:deny-set-position" }, { - "description": "core:window:deny-toggle-maximize -> Denies the toggle_maximize command without any pre-configured scope.", + "description": "Denies the set_progress_bar command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-toggle-maximize" - ] + "const": "core:window:deny-set-progress-bar" }, { - "description": "core:window:deny-unmaximize -> Denies the unmaximize command without any pre-configured scope.", + "description": "Denies the set_resizable command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-unmaximize" - ] + "const": "core:window:deny-set-resizable" }, { - "description": "core:window:deny-unminimize -> Denies the unminimize command without any pre-configured scope.", + "description": "Denies the set_shadow command without any pre-configured scope.", "type": "string", - "enum": [ - "core:window:deny-unminimize" - ] + "const": "core:window:deny-set-shadow" }, { - "description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n", + "description": "Denies the set_size command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:default" - ] + "const": "core:window:deny-set-size" }, { - "description": "dialog:allow-ask -> Enables the ask command without any pre-configured scope.", + "description": "Denies the set_size_constraints command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-ask" - ] + "const": "core:window:deny-set-size-constraints" }, { - "description": "dialog:allow-confirm -> Enables the confirm command without any pre-configured scope.", + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-confirm" - ] + "const": "core:window:deny-set-skip-taskbar" }, { - "description": "dialog:allow-message -> Enables the message command without any pre-configured scope.", + "description": "Denies the set_theme command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-message" - ] + "const": "core:window:deny-set-theme" }, { - "description": "dialog:allow-open -> Enables the open command without any pre-configured scope.", + "description": "Denies the set_title command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-open" - ] + "const": "core:window:deny-set-title" }, { - "description": "dialog:allow-save -> Enables the save command without any pre-configured scope.", + "description": "Denies the set_title_bar_style command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:allow-save" - ] + "const": "core:window:deny-set-title-bar-style" }, { - "description": "dialog:deny-ask -> Denies the ask command without any pre-configured scope.", + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-ask" - ] + "const": "core:window:deny-set-visible-on-all-workspaces" }, { - "description": "dialog:deny-confirm -> Denies the confirm command without any pre-configured scope.", + "description": "Denies the show command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-confirm" - ] + "const": "core:window:deny-show" }, { - "description": "dialog:deny-message -> Denies the message command without any pre-configured scope.", + "description": "Denies the start_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-message" - ] + "const": "core:window:deny-start-dragging" }, { - "description": "dialog:deny-open -> Denies the open command without any pre-configured scope.", + "description": "Denies the start_resize_dragging command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-open" - ] + "const": "core:window:deny-start-resize-dragging" }, { - "description": "dialog:deny-save -> Denies the save command without any pre-configured scope.", + "description": "Denies the theme command without any pre-configured scope.", "type": "string", - "enum": [ - "dialog:deny-save" - ] + "const": "core:window:deny-theme" }, { - "description": "log:default -> Allows the log command", + "description": "Denies the title command without any pre-configured scope.", "type": "string", - "enum": [ - "log:default" - ] + "const": "core:window:deny-title" }, { - "description": "log:allow-log -> Enables the log command without any pre-configured scope.", + "description": "Denies the toggle_maximize command without any pre-configured scope.", "type": "string", - "enum": [ - "log:allow-log" - ] + "const": "core:window:deny-toggle-maximize" }, { - "description": "log:deny-log -> Denies the log command without any pre-configured scope.", + "description": "Denies the unmaximize command without any pre-configured scope.", "type": "string", - "enum": [ - "log:deny-log" - ] + "const": "core:window:deny-unmaximize" }, { - "description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n", + "description": "Denies the unminimize command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:default" - ] + "const": "core:window:deny-unminimize" }, { - "description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.", + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n", "type": "string", - "enum": [ - "notification:allow-batch" - ] + "const": "dialog:default" }, { - "description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.", + "description": "Enables the ask command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-cancel" - ] + "const": "dialog:allow-ask" }, { - "description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.", + "description": "Enables the confirm command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-check-permissions" - ] + "const": "dialog:allow-confirm" }, { - "description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.", + "description": "Enables the message command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-create-channel" - ] + "const": "dialog:allow-message" }, { - "description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.", + "description": "Enables the open command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-delete-channel" - ] + "const": "dialog:allow-open" }, { - "description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.", + "description": "Enables the save command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-get-active" - ] + "const": "dialog:allow-save" }, { - "description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.", + "description": "Denies the ask command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-get-pending" - ] + "const": "dialog:deny-ask" }, { - "description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.", + "description": "Denies the confirm command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-is-permission-granted" - ] + "const": "dialog:deny-confirm" }, { - "description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.", + "description": "Denies the message command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-list-channels" - ] + "const": "dialog:deny-message" }, { - "description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.", + "description": "Denies the open command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-notify" - ] + "const": "dialog:deny-open" }, { - "description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.", + "description": "Denies the save command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-permission-state" - ] + "const": "dialog:deny-save" }, { - "description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.", + "description": "Allows the log command", "type": "string", - "enum": [ - "notification:allow-register-action-types" - ] + "const": "log:default" }, { - "description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.", + "description": "Enables the log command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-register-listener" - ] + "const": "log:allow-log" }, { - "description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.", + "description": "Denies the log command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-remove-active" - ] + "const": "log:deny-log" }, { - "description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.", + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n", "type": "string", - "enum": [ - "notification:allow-request-permission" - ] + "const": "notification:default" }, { - "description": "notification:allow-show -> Enables the show command without any pre-configured scope.", + "description": "Enables the batch command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:allow-show" - ] + "const": "notification:allow-batch" }, { - "description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.", + "description": "Enables the cancel command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-batch" - ] + "const": "notification:allow-cancel" }, { - "description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.", + "description": "Enables the check_permissions command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-cancel" - ] + "const": "notification:allow-check-permissions" }, { - "description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.", + "description": "Enables the create_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-check-permissions" - ] + "const": "notification:allow-create-channel" }, { - "description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.", + "description": "Enables the delete_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-create-channel" - ] + "const": "notification:allow-delete-channel" }, { - "description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.", + "description": "Enables the get_active command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-delete-channel" - ] + "const": "notification:allow-get-active" }, { - "description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.", + "description": "Enables the get_pending command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-get-active" - ] + "const": "notification:allow-get-pending" }, { - "description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.", + "description": "Enables the is_permission_granted command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-get-pending" - ] + "const": "notification:allow-is-permission-granted" }, { - "description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.", + "description": "Enables the list_channels command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-is-permission-granted" - ] + "const": "notification:allow-list-channels" }, { - "description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.", + "description": "Enables the notify command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-list-channels" - ] + "const": "notification:allow-notify" }, { - "description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.", + "description": "Enables the permission_state command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-notify" - ] + "const": "notification:allow-permission-state" }, { - "description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.", + "description": "Enables the register_action_types command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-permission-state" - ] + "const": "notification:allow-register-action-types" }, { - "description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.", + "description": "Enables the register_listener command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-register-action-types" - ] + "const": "notification:allow-register-listener" }, { - "description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.", + "description": "Enables the remove_active command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-register-listener" - ] + "const": "notification:allow-remove-active" }, { - "description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.", + "description": "Enables the request_permission command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-remove-active" - ] + "const": "notification:allow-request-permission" }, { - "description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.", + "description": "Enables the show command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-request-permission" - ] + "const": "notification:allow-show" }, { - "description": "notification:deny-show -> Denies the show command without any pre-configured scope.", + "description": "Denies the batch command without any pre-configured scope.", "type": "string", - "enum": [ - "notification:deny-show" - ] + "const": "notification:deny-batch" }, { - "description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n", + "description": "Denies the cancel command without any pre-configured scope.", "type": "string", - "enum": [ - "os:default" - ] + "const": "notification:deny-cancel" }, { - "description": "os:allow-arch -> Enables the arch command without any pre-configured scope.", + "description": "Denies the check_permissions command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-arch" - ] + "const": "notification:deny-check-permissions" }, { - "description": "os:allow-exe-extension -> Enables the exe_extension command without any pre-configured scope.", + "description": "Denies the create_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-exe-extension" - ] + "const": "notification:deny-create-channel" }, { - "description": "os:allow-family -> Enables the family command without any pre-configured scope.", + "description": "Denies the delete_channel command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-family" - ] + "const": "notification:deny-delete-channel" }, { - "description": "os:allow-hostname -> Enables the hostname command without any pre-configured scope.", + "description": "Denies the get_active command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-hostname" - ] + "const": "notification:deny-get-active" }, { - "description": "os:allow-locale -> Enables the locale command without any pre-configured scope.", + "description": "Denies the get_pending command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-locale" - ] + "const": "notification:deny-get-pending" }, { - "description": "os:allow-os-type -> Enables the os_type command without any pre-configured scope.", + "description": "Denies the is_permission_granted command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-os-type" - ] + "const": "notification:deny-is-permission-granted" }, { - "description": "os:allow-platform -> Enables the platform command without any pre-configured scope.", + "description": "Denies the list_channels command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-platform" - ] + "const": "notification:deny-list-channels" }, { - "description": "os:allow-version -> Enables the version command without any pre-configured scope.", + "description": "Denies the notify command without any pre-configured scope.", "type": "string", - "enum": [ - "os:allow-version" - ] + "const": "notification:deny-notify" }, { - "description": "os:deny-arch -> Denies the arch command without any pre-configured scope.", + "description": "Denies the permission_state command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-arch" - ] + "const": "notification:deny-permission-state" }, { - "description": "os:deny-exe-extension -> Denies the exe_extension command without any pre-configured scope.", + "description": "Denies the register_action_types command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-exe-extension" - ] + "const": "notification:deny-register-action-types" }, { - "description": "os:deny-family -> Denies the family command without any pre-configured scope.", + "description": "Denies the register_listener command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-family" - ] + "const": "notification:deny-register-listener" }, { - "description": "os:deny-hostname -> Denies the hostname command without any pre-configured scope.", + "description": "Denies the remove_active command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-hostname" - ] + "const": "notification:deny-remove-active" }, { - "description": "os:deny-locale -> Denies the locale command without any pre-configured scope.", + "description": "Denies the request_permission command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-locale" - ] + "const": "notification:deny-request-permission" }, { - "description": "os:deny-os-type -> Denies the os_type command without any pre-configured scope.", + "description": "Denies the show command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-os-type" - ] + "const": "notification:deny-show" }, { - "description": "os:deny-platform -> Denies the platform command without any pre-configured scope.", + "description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n", "type": "string", - "enum": [ - "os:deny-platform" - ] + "const": "os:default" }, { - "description": "os:deny-version -> Denies the version command without any pre-configured scope.", + "description": "Enables the arch command without any pre-configured scope.", "type": "string", - "enum": [ - "os:deny-version" - ] + "const": "os:allow-arch" }, { - "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", + "description": "Enables the exe_extension command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:default" - ] + "const": "os:allow-exe-extension" }, { - "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.", + "description": "Enables the family command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-execute" - ] + "const": "os:allow-family" }, { - "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.", + "description": "Enables the hostname command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-kill" - ] + "const": "os:allow-hostname" }, { - "description": "shell:allow-open -> Enables the open command without any pre-configured scope.", + "description": "Enables the locale command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-open" - ] + "const": "os:allow-locale" }, { - "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.", + "description": "Enables the os_type command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-spawn" - ] + "const": "os:allow-os-type" }, { - "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.", + "description": "Enables the platform command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:allow-stdin-write" - ] + "const": "os:allow-platform" }, { - "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.", + "description": "Enables the version command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-execute" - ] + "const": "os:allow-version" }, { - "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.", + "description": "Denies the arch command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-kill" - ] + "const": "os:deny-arch" }, { - "description": "shell:deny-open -> Denies the open command without any pre-configured scope.", + "description": "Denies the exe_extension command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-open" - ] + "const": "os:deny-exe-extension" }, { - "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.", + "description": "Denies the family command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-spawn" - ] + "const": "os:deny-family" }, { - "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.", + "description": "Denies the hostname command without any pre-configured scope.", "type": "string", - "enum": [ - "shell:deny-stdin-write" - ] + "const": "os:deny-hostname" }, { - "description": "window-state:default -> This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n", + "description": "Denies the locale command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:default" - ] + "const": "os:deny-locale" }, { - "description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.", + "description": "Denies the os_type command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:allow-filename" - ] + "const": "os:deny-os-type" }, { - "description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.", + "description": "Denies the platform command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:allow-restore-state" - ] + "const": "os:deny-platform" }, { - "description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.", + "description": "Denies the version command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:allow-save-window-state" - ] + "const": "os:deny-version" }, { - "description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.", + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", "type": "string", - "enum": [ - "window-state:deny-filename" - ] + "const": "shell:default" }, { - "description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.", + "description": "Enables the execute command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:deny-restore-state" - ] + "const": "shell:allow-execute" }, { - "description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.", + "description": "Enables the kill command without any pre-configured scope.", "type": "string", - "enum": [ - "window-state:deny-save-window-state" - ] + "const": "shell:allow-kill" + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open" + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn" + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write" + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute" + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill" + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open" + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn" + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write" + }, + { + "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n", + "type": "string", + "const": "window-state:default" + }, + { + "description": "Enables the filename command without any pre-configured scope.", + "type": "string", + "const": "window-state:allow-filename" + }, + { + "description": "Enables the restore_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:allow-restore-state" + }, + { + "description": "Enables the save_window_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:allow-save-window-state" + }, + { + "description": "Denies the filename command without any pre-configured scope.", + "type": "string", + "const": "window-state:deny-filename" + }, + { + "description": "Denies the restore_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:deny-restore-state" + }, + { + "description": "Denies the save_window_state command without any pre-configured scope.", + "type": "string", + "const": "window-state:deny-save-window-state" } ] }, @@ -3027,7 +2426,7 @@ } ] }, - "ShellAllowedArg": { + "ShellScopeEntryAllowedArg": { "description": "A command argument allowed to be executed by the webview API.", "anyOf": [ { @@ -3055,18 +2454,18 @@ } ] }, - "ShellAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "ShellScopeEntryAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", "anyOf": [ { "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", "type": "boolean" }, { - "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.", + "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", "type": "array", "items": { - "$ref": "#/definitions/ShellAllowedArg" + "$ref": "#/definitions/ShellScopeEntryAllowedArg" } } ] diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 634bca75..0983db28 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -185,8 +185,10 @@ pub fn setup_tray_menu( app.dialog() .message("This does not stop the Portmaster system service") .title("Do you really want to quit the user interface?") - .ok_button_label("Yes, exit") - .cancel_button_label("No") + .buttons(tauri_plugin_dialog::MessageDialogButtons::OkCancelCustom( + "Yes, exit".to_owned(), + "No".to_owned(), + )) .show(move |answer| { if answer { // let _ = handle.emit("exit-requested", ""); From 2958a0e160628abd45f432da5541d76ac3e3d264 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 11:44:29 +0300 Subject: [PATCH 41/62] [WIP] Trigger release workflow --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d0850c5..27af01c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,6 @@ name: Release on: push: - paths: - - '.github/workflows/release.yml' workflow_dispatch: jobs: From 6b45c8ad3c2cdb568a9ae606af1e435cad8a5f50 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 12:47:57 +0300 Subject: [PATCH 42/62] [WIP] Fix earthly build --- Earthfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Earthfile b/Earthfile index f5a24139..21ceee2c 100644 --- a/Earthfile +++ b/Earthfile @@ -538,8 +538,8 @@ release-prep: RUN touch "./output/intel/urgent.dsdl" COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr - RUN ./updatemgr -dir "./output/binary" -name "Binary" > ./output/binary/bin-index.json - RUN ./updatemgr -dir "./output/intel" -name "Intel" > ./output/intel/intel-index.json + RUN ./updatemgr scan --dir "./output/binary" > ./output/binary/bin-index.json + RUN ./updatemgr scan --dir "./output/intel" > ./output/intel/intel-index.json # Intel Extracted (needed for the installers) RUN mkdir -p ./output/intel_decompressed From 35cc0c36fa26a2b56e8293450d89fbbae0131518 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 13:43:10 +0300 Subject: [PATCH 43/62] [WIP] Fix windows release CI --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27af01c6..a142c83a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,8 +54,6 @@ jobs: name: Installer windows runs-on: windows-latest needs: release-prep - container: - image: abrarov/msvc-2022:latest steps: - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 From 993505bf2c946d17f322b85fe7f4b262f03736fe Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 15:47:59 +0300 Subject: [PATCH 44/62] [WIP] Fix CI artifact upload --- .github/workflows/release.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a142c83a..5584cbfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,12 +23,11 @@ jobs: - name: Build all artifacts run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +release-prep - - - name: Upload Dist uses: actions/upload-artifact@v4 with: name: dist path: dist/ + if-no-files-found: error installer-linux: name: Installer linux @@ -62,12 +61,9 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Download Dist + - name: Build windows artifacts + run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 uses: actions/download-artifact@v4 with: name: dist path: dist/ - - - name: Build windows artifacts - run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 - From 5127cdb72dab15ecc865fb05a1ac17fac9fd20d0 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 16:10:09 +0300 Subject: [PATCH 45/62] [WIP] Fix CI build --- .github/workflows/release.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5584cbfa..9a857aba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,11 +22,13 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build all artifacts - run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +release-prep + run: earthly --remote-cache=ghcr.io/safing/build-cache --push +release-prep + + - name: Upload Dist uses: actions/upload-artifact@v4 with: name: dist - path: dist/ + path: ./dist/ if-no-files-found: error installer-linux: @@ -48,6 +50,7 @@ jobs: - name: Build linux installers run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +installer-linux + # --ci include --no-output flag installer-windows: name: Installer windows @@ -61,9 +64,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build windows artifacts - run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 + - name: Download Dist uses: actions/download-artifact@v4 with: name: dist path: dist/ + + - name: Build windows artifacts + run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 + From 7192d3b034effce6e2a039cfc6e46e13ed446014 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 16:43:23 +0300 Subject: [PATCH 46/62] [WIP] Fix windows CI build --- .github/workflows/release.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a857aba..8b5e40cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,12 +57,8 @@ jobs: runs-on: windows-latest needs: release-prep steps: - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout Repository + uses: actions/checkout@v4 - name: Download Dist uses: actions/download-artifact@v4 From 3502eb2c549c46d462e6d07e34e09f3b2ed95eb9 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 17:09:57 +0300 Subject: [PATCH 47/62] [WIP] Fix windows CI --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b5e40cd..7c3f3321 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,5 +67,5 @@ jobs: path: dist/ - name: Build windows artifacts - run: powershell -NoProfile -File ./packaging/windows/generate_windows_installer.ps1 + run: powershell -NoProfile -File ./packaging/windows/generate_windows_installers.ps1 From 6be733062178c8c5657a2380d8e4a06f45dae746 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 17:41:16 +0300 Subject: [PATCH 48/62] [WIP] Fix windows installer CI --- .../windows/generate_windows_installers.ps1 | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 index f55f0d89..85049f59 100644 --- a/packaging/windows/generate_windows_installers.ps1 +++ b/packaging/windows/generate_windows_installers.ps1 @@ -12,21 +12,28 @@ $originalDirectory = Get-Location $destinationDir = "desktop/tauri/src-tauri" $binaryDir = "$destinationDir/binary" $intelDir = "$destinationDir/intel" +$targetDir = "$destinationDir/target/release" -# Make sure distination folder exists. +# Make sure binary folder exists. if (-not (Test-Path -Path $binaryDir)) { New-Item -ItemType Directory -Path $binaryDir > $null } + Write-Output "Copying binary files" Copy-Item -Force -Path "dist/binary/bin-index.json" -Destination "$binaryDir/bin-index.json" Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-core.exe" -Destination "$binaryDir/portmaster-core.exe" Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-kext.sys" -Destination "$binaryDir/portmaster-kext.sys" Copy-Item -Force -Path "dist/binary/all/portmaster.zip" -Destination "$binaryDir/portmaster.zip" Copy-Item -Force -Path "dist/binary/all/assets.zip" -Destination "$binaryDir/assets.zip" -Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster.exe" -Destination "$destinationDir/target/release/portmaster.exe" -# Make sure distination folder exists. +# Make sure target folder exists. +if (-not (Test-Path -Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir > $null +} +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster.exe" -Destination "$targetDir/portmaster.exe" + +# Make sure intel folder exists. if (-not (Test-Path -Path $intelDir)) { New-Item -ItemType Directory -Path $intelDir > $null } @@ -61,5 +68,29 @@ if (-not (Test-Path -Path $installerDist)) { Copy-Item -Path ".\target\release\bundle\msi\*" -Destination $installerDist Copy-Item -Path ".\target\release\bundle\nsis\*" -Destination $installerDist + # Restore the original directory -Set-Location $originalDirectory \ No newline at end of file +Set-Location $originalDirectory + + +# FIXME: remove +function Show-Tree { + param ( + [string]$Path = ".", + [int]$Indent = 0 + ) + + $items = Get-ChildItem -Path $Path + + foreach ($item in $items) { + Write-Host (" " * $Indent) -NoNewline + Write-Host "+-- " -NoNewline + Write-Host $item.Name + + if ($item.PSIsContainer) { + Show-Tree -Path $item.FullName -Indent ($Indent + 4) + } + } +} + +Show-Tree 'C:\Program Files (x86)\Windows Kits\10\Include' From eb74b6cc1dae1592ad5d99340b39000b7bf2be23 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 11 Oct 2024 19:23:06 +0300 Subject: [PATCH 49/62] [WIP] Fix path of the windows installers --- desktop/tauri/src-tauri/templates/files.wxs | 24 ++++++------- .../templates/nsis_install_hooks.nsh | 36 +++++++++---------- .../windows/generate_windows_installers.ps1 | 22 ------------ 3 files changed, 28 insertions(+), 54 deletions(-) diff --git a/desktop/tauri/src-tauri/templates/files.wxs b/desktop/tauri/src-tauri/templates/files.wxs index d69ec0f9..4828178f 100644 --- a/desktop/tauri/src-tauri/templates/files.wxs +++ b/desktop/tauri/src-tauri/templates/files.wxs @@ -12,21 +12,21 @@ - - - - - + + + + + - - - - - - - + + + + + + + diff --git a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh index b2ddd628..6b5afb58 100644 --- a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh +++ b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh @@ -1,23 +1,23 @@ !macro NSIS_HOOK_PREINSTALL - ; Current working directory is \target\release\nsis\x64 + ; Current working directory is \desktop\tauri\src-tauri\target\release\nsis\x64 SetOutPath "$INSTDIR" - File "..\..\..\..\binaries\bin-index.json" - File "..\..\..\..\binaries\portmaster-core.exe" - File "..\..\..\..\binaries\portmaster-kext.sys" - File "..\..\..\..\binaries\portmaster.zip" - File "..\..\..\..\binaries\assets.zip" + File "..\..\..\..\binary\bin-index.json" + File "..\..\..\..\binary\portmaster-core.exe" + File "..\..\..\..\binary\portmaster-kext.sys" + File "..\..\..\..\binary\portmaster.zip" + File "..\..\..\..\binary\assets.zip" SetOutPath "$COMMONPROGRAMDATA\Portmaster\intel" - File "..\..\..\..\binaries\intel-index.json" - File "..\..\..\..\binaries\base.dsdl" - File "..\..\..\..\binaries\geoipv4.mmdb" - File "..\..\..\..\binaries\geoipv6.mmdb" - File "..\..\..\..\binaries\index.dsd" - File "..\..\..\..\binaries\intermediate.dsdl" - File "..\..\..\..\binaries\urgent.dsdl" + File "..\..\..\..\intel\intel-index.json" + File "..\..\..\..\intel\base.dsdl" + File "..\..\..\..\intel\geoipv4.mmdb" + File "..\..\..\..\intel\geoipv6.mmdb" + File "..\..\..\..\intel\index.dsd" + File "..\..\..\..\intel\intermediate.dsdl" + File "..\..\..\..\intel\urgent.dsdl" ; restire previous state SetOutPath "$INSTDIR" @@ -25,15 +25,11 @@ !macroend !macro NSIS_HOOK_POSTINSTALL - ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"' $0 - IntCmp $0 0 +2 - MessageBox MB_OK "Failed to create PortmasterCore service." + ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"' !macroend !macro NSIS_HOOK_PREUNINSTALL - ExecWait 'sc.exe stop PortmasterCore' $0 - ; Ignore errors if the service is not running - ExecWait 'sc.exe delete PortmasterCore' $0 - ; Ignore errors if the service does not exist + ExecWait 'sc.exe stop PortmasterCore' + ExecWait 'sc.exe delete PortmasterCore' !macroend diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 index 85049f59..9027025c 100644 --- a/packaging/windows/generate_windows_installers.ps1 +++ b/packaging/windows/generate_windows_installers.ps1 @@ -72,25 +72,3 @@ Copy-Item -Path ".\target\release\bundle\nsis\*" -Destination $installerDist # Restore the original directory Set-Location $originalDirectory - -# FIXME: remove -function Show-Tree { - param ( - [string]$Path = ".", - [int]$Indent = 0 - ) - - $items = Get-ChildItem -Path $Path - - foreach ($item in $items) { - Write-Host (" " * $Indent) -NoNewline - Write-Host "+-- " -NoNewline - Write-Host $item.Name - - if ($item.PSIsContainer) { - Show-Tree -Path $item.FullName -Indent ($Indent + 4) - } - } -} - -Show-Tree 'C:\Program Files (x86)\Windows Kits\10\Include' From d6669ff8f58adaa3f852e01e15b19e69f61e49ee Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 16 Oct 2024 13:51:38 +0200 Subject: [PATCH 50/62] Remove unpack suffixes when generating an index bundle --- Earthfile | 2 +- cmds/updatemgr/scan.go | 13 ++++++++++--- service/updates/bundlegeneration.go | 5 +++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Earthfile b/Earthfile index 21ceee2c..94223265 100644 --- a/Earthfile +++ b/Earthfile @@ -177,7 +177,7 @@ go-build: ARG GOOS=linux ARG GOARCH=amd64 ARG GOARM - ARG CMDS=portmaster-start portmaster-core + ARG CMDS=portmaster-core CACHE --sharing shared "$GOCACHE" CACHE --sharing shared "$GOMODCACHE" diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go index fde03eff..25d5bce7 100644 --- a/cmds/updatemgr/scan.go +++ b/cmds/updatemgr/scan.go @@ -31,15 +31,22 @@ var ( "**/hubs*.json", "**/*mini*.mmdb.gz", + // Unsupported platforms. + "darwin_amd64/**", + "darwin_arm64/**", + // Deprecated artifacts. + "**/portmaster-start*", + "**/portmaster-app*", + "**/portmaster-notifier*", + "**/portmaster-wintoast*.dll", + "**/portmaster-snoretoast*.exe", + "**/portmaster-kext*.dll", "**/profilemgr*.zip", "**/settings*.zip", "**/monitor*.zip", "**/base*.zip", "**/console*.zip", - "**/portmaster-wintoast*.dll", - "**/portmaster-snoretoast*.exe", - "**/portmaster-kext*.dll", }, UnpackFiles: map[string]string{ "gz": "**/*.gz", diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go index b7117926..45871edc 100644 --- a/service/updates/bundlegeneration.go +++ b/service/updates/bundlegeneration.go @@ -209,6 +209,11 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund artifact.Version = version } + // Remove unpack suffix. + if artifact.Unpack != "" { + artifact.Filename, _ = strings.CutSuffix(artifact.Filename, "."+artifact.Unpack) + } + // Set local file path. artifact.localFile = fullpath From 68f5bf73e1b8a26dc186698c20047333e2f602cb Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 18 Oct 2024 11:53:17 +0300 Subject: [PATCH 51/62] [github] Remove push from release workflow --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c3f3321..2ec7fa30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ name: Release on: - push: workflow_dispatch: jobs: From d6812bb4ef1cbc658cb4220eef25adc960ff6bb6 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Fri, 18 Oct 2024 16:45:47 +0300 Subject: [PATCH 52/62] [WIP] Remove updater index filename setting --- Earthfile | 12 ++++++------ packaging/windows/generate_windows_installers.ps1 | 2 +- service/instance.go | 12 ++++-------- service/updates/downloader.go | 6 ++---- service/updates/module.go | 6 +++--- service/updates/registry.go | 2 +- 6 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Earthfile b/Earthfile index 94223265..dca57816 100644 --- a/Earthfile +++ b/Earthfile @@ -538,12 +538,12 @@ release-prep: RUN touch "./output/intel/urgent.dsdl" COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr - RUN ./updatemgr scan --dir "./output/binary" > ./output/binary/bin-index.json - RUN ./updatemgr scan --dir "./output/intel" > ./output/intel/intel-index.json + RUN ./updatemgr scan --dir "./output/binary" > ./output/binary/index.json + RUN ./updatemgr scan --dir "./output/intel" > ./output/intel/index.json # Intel Extracted (needed for the installers) RUN mkdir -p ./output/intel_decompressed - RUN cp ./output/intel/intel-index.json ./output/intel_decompressed/intel-index.json + RUN cp ./output/intel/index.json ./output/intel_decompressed/index.json RUN gzip -dc ./output/intel/geoipv4.mmdb.gz > ./output/intel_decompressed/geoipv4.mmdb RUN gzip -dc ./output/intel/geoipv6.mmdb.gz > ./output/intel_decompressed/geoipv6.mmdb RUN touch "./output/intel_decompressed/index.dsd" @@ -552,7 +552,7 @@ release-prep: RUN touch "./output/intel_decompressed/urgent.dsdl" # Save all artifacts to output folder - SAVE ARTIFACT --if-exists --keep-ts "output/binary/bin-index.json" AS LOCAL "${outputDir}/binary/bin-index.json" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/index.json" AS LOCAL "${outputDir}/binary/index.json" SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" AS LOCAL "${outputDir}/binary/all/" SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" AS LOCAL "${outputDir}/binary/linux_amd64/" SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" AS LOCAL "${outputDir}/binary/windows_amd64/" @@ -560,7 +560,7 @@ release-prep: SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" AS LOCAL "${outputDir}/intel_decompressed/" # Save all artifacts to the container output folder so other containers can access it. - SAVE ARTIFACT --if-exists --keep-ts "output/binary/bin-index.json" "output/binary/bin-index.json" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/index.json" "output/binary/index.json" SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" "output/binary/all/" SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" "output/binary/linux_amd64/" SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" "output/binary/windows_amd64/" @@ -588,7 +588,7 @@ installer-linux: COPY (+release-prep/output/binary/linux_amd64/portmaster) ./target/${target}/release/portmaster RUN mkdir -p binary - COPY (+release-prep/output/binary/bin-index.json) ./binary/bin-index.json + COPY (+release-prep/output/binary/index.json) ./binary/index.json COPY (+release-prep/output/binary/linux_amd64/portmaster-core) ./binary/portmaster-core COPY (+release-prep/output/binary/all/portmaster.zip) ./binary/portmaster.zip COPY (+release-prep/output/binary/all/assets.zip) ./binary/assets.zip diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 index 9027025c..c18a0d1b 100644 --- a/packaging/windows/generate_windows_installers.ps1 +++ b/packaging/windows/generate_windows_installers.ps1 @@ -21,7 +21,7 @@ if (-not (Test-Path -Path $binaryDir)) { Write-Output "Copying binary files" -Copy-Item -Force -Path "dist/binary/bin-index.json" -Destination "$binaryDir/bin-index.json" +Copy-Item -Force -Path "dist/binary/index.json" -Destination "$binaryDir/index.json" Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-core.exe" -Destination "$binaryDir/portmaster-core.exe" Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-kext.sys" -Destination "$binaryDir/portmaster-kext.sys" Copy-Item -Force -Path "dist/binary/all/portmaster.zip" -Destination "$binaryDir/portmaster.zip" diff --git a/service/instance.go b/service/instance.go index 67682f1b..52d40f93 100644 --- a/service/instance.go +++ b/service/instance.go @@ -138,8 +138,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_binary"), PurgeDirectory: filepath.Join(binaryFolder, "old_binary"), // Default: C:/Program Files/Portmaster/old_binary Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, - IndexFile: "bin-index.json", + IndexURLs: []string{"https://updates.safing.io/stable.v3.json"}, AutoApply: false, NeedsRestart: true, } @@ -148,8 +147,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx Directory: os.ExpandEnv("$ProgramData/Portmaster/intel"), DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_intel"), PurgeDirectory: os.ExpandEnv("$ProgramData/Portmaster/old_intel"), - IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, - IndexFile: "intel-index.json", + IndexURLs: []string{"https://updates.safing.io/intel.v3.json"}, AutoApply: true, NeedsRestart: false, } @@ -159,8 +157,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx DownloadDirectory: "/var/lib/portmaster/new_bin", PurgeDirectory: "/var/lib/portmaster/old_bin", Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", + IndexURLs: []string{"https://updates.safing.io/stable.v3.json"}, AutoApply: false, NeedsRestart: true, } @@ -169,8 +166,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx Directory: "/var/lib/portmaster/intel", DownloadDirectory: "/var/lib/portmaster/new_intel", PurgeDirectory: "/var/lib/portmaster/intel_bin", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", + IndexURLs: []string{"https://updates.safing.io/intel.v3.json"}, AutoApply: true, NeedsRestart: false, } diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 84259901..40150235 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -22,7 +22,6 @@ import ( type Downloader struct { dir string - indexFile string indexURLs []string bundle *Bundle version *semver.Version @@ -33,7 +32,6 @@ type Downloader struct { func CreateDownloader(index UpdateIndex) Downloader { return Downloader{ dir: index.DownloadDirectory, - indexFile: index.IndexFile, indexURLs: index.IndexURLs, } } @@ -78,7 +76,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { } // Write the content into a file. - indexFilepath := filepath.Join(d.dir, d.indexFile) + indexFilepath := filepath.Join(d.dir, indexFilename) err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode) if err != nil { return fmt.Errorf("failed to write index file: %w", err) @@ -98,7 +96,7 @@ func (d *Downloader) Verify() error { } func (d *Downloader) parseBundle() error { - indexFilepath := filepath.Join(d.dir, d.indexFile) + indexFilepath := filepath.Join(d.dir, indexFilename) var err error d.bundle, err = LoadBundle(indexFilepath) if err != nil { diff --git a/service/updates/module.go b/service/updates/module.go index f33c830c..c6486043 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -18,6 +18,8 @@ const ( updateFailedNotificationID = "updates:update-failed" corruptInstallationNotificationID = "updates:corrupt-installation" + indexFilename = "index.json" + // ResourceUpdateEvent is emitted every time the // updater successfully performed a resource update. ResourceUpdateEvent = "resource update" @@ -39,7 +41,6 @@ type UpdateIndex struct { PurgeDirectory string Ignore []string IndexURLs []string - IndexFile string AutoApply bool NeedsRestart bool } @@ -176,7 +177,6 @@ func (u *Updates) UpdateFromURL(url string) error { index := UpdateIndex{ DownloadDirectory: u.downloader.dir, IndexURLs: []string{url}, - IndexFile: u.downloader.indexFile, } // Initialize with proper values and download the index file. @@ -214,7 +214,7 @@ func (u *Updates) applyUpdates(downloader Downloader, force bool) error { log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) } - err := u.registry.performRecoverableUpgrade(downloader.dir, downloader.indexFile) + err := u.registry.performRecoverableUpgrade(downloader.dir, indexFilename) if err != nil { // Notify the user that update failed. notifications.NotifyPrompt(updateFailedNotificationID, "Failed to apply update.", err.Error()) diff --git a/service/updates/registry.go b/service/updates/registry.go index 98df62a0..e73b272e 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -36,7 +36,7 @@ func CreateRegistry(index UpdateIndex) (Registry, error) { } // Parse bundle var err error - registry.bundle, err = LoadBundle(filepath.Join(index.Directory, index.IndexFile)) + registry.bundle, err = LoadBundle(filepath.Join(index.Directory, indexFilename)) if err != nil { return Registry{}, err } From 68e37462710ac89996016eb47000bdab19055999 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Mon, 28 Oct 2024 08:55:32 +0200 Subject: [PATCH 53/62] [WIP] Download real intel data while building --- Earthfile | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Earthfile b/Earthfile index dca57816..8937bd05 100644 --- a/Earthfile +++ b/Earthfile @@ -529,13 +529,12 @@ release-prep: # Intel # TODO(vladimir): figure out a way to download all latest intel data. RUN mkdir -p ./output/intel - RUN wget -O ./output/intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240529-0-1.mmdb.gz" && \ - wget -O ./output/intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240529-0-1.mmdb.gz" - - RUN touch "./output/intel/index.dsd" - RUN touch "./output/intel/base.dsdl" - RUN touch "./output/intel/intermediate.dsdl" - RUN touch "./output/intel/urgent.dsdl" + RUN wget -O ./output/intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240820-0-1.mmdb.gz" && \ + wget -O ./output/intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240820-0-1.mmdb.gz" && \ + wget -O ./output/intel/index.dsd "https://updates.safing.io/all/intel/lists/index_v2023-6-13.dsd" && \ + wget -O ./output/intel/base.dsdl "https://updates.safing.io/all/intel/lists/base_v20241001-0-9.dsdl" && \ + wget -O ./output/intel/intermediate.dsdl "https://updates.safing.io/all/intel/lists/intermediate_v20240929-0-0.dsdl" && \ + wget -O ./output/intel/urgent.dsdl "https://updates.safing.io/all/intel/lists/urgent_v20241002-2-14.dsdl" COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr RUN ./updatemgr scan --dir "./output/binary" > ./output/binary/index.json @@ -546,10 +545,10 @@ release-prep: RUN cp ./output/intel/index.json ./output/intel_decompressed/index.json RUN gzip -dc ./output/intel/geoipv4.mmdb.gz > ./output/intel_decompressed/geoipv4.mmdb RUN gzip -dc ./output/intel/geoipv6.mmdb.gz > ./output/intel_decompressed/geoipv6.mmdb - RUN touch "./output/intel_decompressed/index.dsd" - RUN touch "./output/intel_decompressed/base.dsdl" - RUN touch "./output/intel_decompressed/intermediate.dsdl" - RUN touch "./output/intel_decompressed/urgent.dsdl" + RUN cp ./output/intel/index.dsd ./output/intel_decompressed/index.dsd + RUN cp ./output/intel/base.dsdl ./output/intel_decompressed/base.dsdl + RUN cp ./output/intel/intermediate.dsdl ./output/intel_decompressed/intermediate.dsdl + RUN cp ./output/intel/urgent.dsdl ./output/intel_decompressed/urgent.dsdl # Save all artifacts to output folder SAVE ARTIFACT --if-exists --keep-ts "output/binary/index.json" AS LOCAL "${outputDir}/binary/index.json" From 0f3f3c360fafa0224108c12946207f4d9e15f563 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 31 Oct 2024 16:38:12 +0100 Subject: [PATCH 54/62] [WIP] Simplify update system --- base/notifications/module.go | 41 ++ cmds/updatemgr/scan.go | 2 +- service/broadcasts/module.go | 2 +- service/core/core.go | 4 +- service/firewall/interception/module.go | 2 +- service/firewall/module.go | 2 +- service/instance.go | 20 +- service/intel/filterlists/module.go | 2 +- service/intel/geoip/init_test.go | 6 +- service/intel/geoip/module.go | 2 +- service/netenv/init_test.go | 6 +- service/netenv/main.go | 2 +- service/process/module.go | 2 +- service/profile/endpoints/endpoints_test.go | 6 +- service/resolver/main_test.go | 6 +- service/ui/module.go | 2 +- service/updates/bundle.go | 142 ------ service/updates/downloader.go | 415 +++++++--------- service/updates/index.go | 295 +++++++++++ .../{bundlegeneration.go => index_scan.go} | 70 +-- service/updates/module.go | 461 +++++++++++------- service/updates/registry.go | 249 ---------- service/updates/signing.go | 29 ++ service/updates/updates_test.go | 2 +- service/updates/upgrade.go | 196 ++++++++ spn/captain/module.go | 2 +- spn/hub/hub_test.go | 6 +- spn/instance.go | 12 +- spn/navigator/module_test.go | 6 +- 29 files changed, 1101 insertions(+), 891 deletions(-) delete mode 100644 service/updates/bundle.go create mode 100644 service/updates/index.go rename service/updates/{bundlegeneration.go => index_scan.go} (84%) delete mode 100644 service/updates/registry.go create mode 100644 service/updates/signing.go create mode 100644 service/updates/upgrade.go diff --git a/base/notifications/module.go b/base/notifications/module.go index f69d017e..bbbb88cd 100644 --- a/base/notifications/module.go +++ b/base/notifications/module.go @@ -36,6 +36,47 @@ func (n *Notifications) Stop() error { return nil } +// NotifyInfo is a helper method for quickly showing an info notification. +// The notification will be activated immediately. +// If the provided id is empty, an id will derived from msg. +// ShowOnSystem is disabled. +// If no actions are defined, a default "OK" (ID:"ack") action will be added. +func (n *Notifications) NotifyInfo(id, title, msg string, actions ...Action) *Notification { + return NotifyInfo(id, title, msg, actions...) +} + +// NotifyWarn is a helper method for quickly showing a warning notification +// The notification will be activated immediately. +// If the provided id is empty, an id will derived from msg. +// ShowOnSystem is enabled. +// If no actions are defined, a default "OK" (ID:"ack") action will be added. +func (n *Notifications) NotifyWarn(id, title, msg string, actions ...Action) *Notification { + return NotifyWarn(id, title, msg, actions...) +} + +// NotifyError is a helper method for quickly showing an error notification. +// The notification will be activated immediately. +// If the provided id is empty, an id will derived from msg. +// ShowOnSystem is enabled. +// If no actions are defined, a default "OK" (ID:"ack") action will be added. +func (n *Notifications) NotifyError(id, title, msg string, actions ...Action) *Notification { + return NotifyError(id, title, msg, actions...) +} + +// NotifyPrompt is a helper method for quickly showing a prompt notification. +// The notification will be activated immediately. +// If the provided id is empty, an id will derived from msg. +// ShowOnSystem is disabled. +// If no actions are defined, a default "OK" (ID:"ack") action will be added. +func (n *Notifications) NotifyPrompt(id, title, msg string, actions ...Action) *Notification { + return NotifyPrompt(id, title, msg, actions...) +} + +// Notify sends the given notification. +func (n *Notifications) Notify(notification *Notification) *Notification { + return Notify(notification) +} + func prep() error { return registerConfig() } diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go index 25d5bce7..06435bf4 100644 --- a/cmds/updatemgr/scan.go +++ b/cmds/updatemgr/scan.go @@ -9,7 +9,7 @@ import ( ) var ( - bundleSettings = updates.BundleFileSettings{ + bundleSettings = updates.IndexScanConfig{ Name: "Portmaster Binaries", PrimaryArtifact: "linux_amd64/portmaster-core", BaseURL: "https://updates.safing.io/", diff --git a/service/broadcasts/module.go b/service/broadcasts/module.go index 19c1641f..a9a87074 100644 --- a/service/broadcasts/module.go +++ b/service/broadcasts/module.go @@ -93,5 +93,5 @@ func New(instance instance) (*Broadcasts, error) { } type instance interface { - IntelUpdates() *updates.Updates + IntelUpdates() *updates.Updater } diff --git a/service/core/core.go b/service/core/core.go index 1f41285c..356bd2eb 100644 --- a/service/core/core.go +++ b/service/core/core.go @@ -116,6 +116,6 @@ type instance interface { Shutdown() Restart() AddWorkerInfoToDebugInfo(di *debug.Info) - BinaryUpdates() *updates.Updates - IntelUpdates() *updates.Updates + BinaryUpdates() *updates.Updater + IntelUpdates() *updates.Updater } diff --git a/service/firewall/interception/module.go b/service/firewall/interception/module.go index 158432e0..63994c90 100644 --- a/service/firewall/interception/module.go +++ b/service/firewall/interception/module.go @@ -99,5 +99,5 @@ func New(instance instance) (*Interception, error) { } type instance interface { - BinaryUpdates() *updates.Updates + BinaryUpdates() *updates.Updater } diff --git a/service/firewall/module.go b/service/firewall/module.go index d6e38bca..056f59bb 100644 --- a/service/firewall/module.go +++ b/service/firewall/module.go @@ -161,7 +161,7 @@ func New(instance instance) (*Firewall, error) { type instance interface { Config() *config.Config - BinaryUpdates() *updates.Updates + BinaryUpdates() *updates.Updater Profile() *profile.ProfileModule Captain() *captain.Captain Access() *access.Access diff --git a/service/instance.go b/service/instance.go index 67682f1b..cb050a99 100644 --- a/service/instance.go +++ b/service/instance.go @@ -67,8 +67,8 @@ type Instance struct { base *base.Base core *core.Core - binaryUpdates *updates.Updates - intelUpdates *updates.Updates + binaryUpdates *updates.Updater + intelUpdates *updates.Updater geoip *geoip.GeoIP netenv *netenv.NetEnv ui *ui.UI @@ -126,14 +126,14 @@ func getCurrentBinaryFolder() (string, error) { // New returns a new Portmaster service instance. func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx - var binaryUpdateIndex updates.UpdateIndex - var intelUpdateIndex updates.UpdateIndex + var binaryUpdateIndex updates.Config + var intelUpdateIndex updates.Config if go_runtime.GOOS == "windows" { binaryFolder, err := getCurrentBinaryFolder() if err != nil { return nil, err } - binaryUpdateIndex = updates.UpdateIndex{ + binaryUpdateIndex = updates.Config{ Directory: binaryFolder, // Default: C:/Program Files/Portmaster DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_binary"), PurgeDirectory: filepath.Join(binaryFolder, "old_binary"), // Default: C:/Program Files/Portmaster/old_binary @@ -144,7 +144,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx NeedsRestart: true, } - intelUpdateIndex = updates.UpdateIndex{ + intelUpdateIndex = updates.Config{ Directory: os.ExpandEnv("$ProgramData/Portmaster/intel"), DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_intel"), PurgeDirectory: os.ExpandEnv("$ProgramData/Portmaster/old_intel"), @@ -154,7 +154,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx NeedsRestart: false, } } else if go_runtime.GOOS == "linux" { - binaryUpdateIndex = updates.UpdateIndex{ + binaryUpdateIndex = updates.Config{ Directory: "/usr/lib/portmaster", DownloadDirectory: "/var/lib/portmaster/new_bin", PurgeDirectory: "/var/lib/portmaster/old_bin", @@ -165,7 +165,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx NeedsRestart: true, } - intelUpdateIndex = updates.UpdateIndex{ + intelUpdateIndex = updates.Config{ Directory: "/var/lib/portmaster/intel", DownloadDirectory: "/var/lib/portmaster/new_intel", PurgeDirectory: "/var/lib/portmaster/intel_bin", @@ -454,12 +454,12 @@ func (i *Instance) Base() *base.Base { } // BinaryUpdates returns the updates module. -func (i *Instance) BinaryUpdates() *updates.Updates { +func (i *Instance) BinaryUpdates() *updates.Updater { return i.binaryUpdates } // IntelUpdates returns the updates module. -func (i *Instance) IntelUpdates() *updates.Updates { +func (i *Instance) IntelUpdates() *updates.Updater { return i.intelUpdates } diff --git a/service/intel/filterlists/module.go b/service/intel/filterlists/module.go index 8f3a5a5e..fd780890 100644 --- a/service/intel/filterlists/module.go +++ b/service/intel/filterlists/module.go @@ -142,6 +142,6 @@ func New(instance instance) (*FilterLists, error) { } type instance interface { - IntelUpdates() *updates.Updates + IntelUpdates() *updates.Updater NetEnv() *netenv.NetEnv } diff --git a/service/intel/geoip/init_test.go b/service/intel/geoip/init_test.go index b56da4ea..9d0288f2 100644 --- a/service/intel/geoip/init_test.go +++ b/service/intel/geoip/init_test.go @@ -16,12 +16,12 @@ type testInstance struct { db *dbmodule.DBModule api *api.API config *config.Config - updates *updates.Updates + updates *updates.Updater } var _ instance = &testInstance{} -func (stub *testInstance) IntelUpdates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updater { return stub.updates } @@ -77,7 +77,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + stub.updates, err = updates.New(stub, "Test Intel", updates.Config{ Directory: installDir, IndexFile: "index.json", }) diff --git a/service/intel/geoip/module.go b/service/intel/geoip/module.go index 2ebde990..01c3797a 100644 --- a/service/intel/geoip/module.go +++ b/service/intel/geoip/module.go @@ -66,5 +66,5 @@ func New(instance instance) (*GeoIP, error) { } type instance interface { - IntelUpdates() *updates.Updates + IntelUpdates() *updates.Updater } diff --git a/service/netenv/init_test.go b/service/netenv/init_test.go index 1c026d68..39c0febd 100644 --- a/service/netenv/init_test.go +++ b/service/netenv/init_test.go @@ -16,12 +16,12 @@ type testInstance struct { db *dbmodule.DBModule api *api.API config *config.Config - updates *updates.Updates + updates *updates.Updater } var _ instance = &testInstance{} -func (stub *testInstance) IntelUpdates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updater { return stub.updates } @@ -77,7 +77,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + stub.updates, err = updates.New(stub, "Test Intel", updates.Config{ Directory: installDir, IndexFile: "index.json", }) diff --git a/service/netenv/main.go b/service/netenv/main.go index 81cd540f..ed985c4b 100644 --- a/service/netenv/main.go +++ b/service/netenv/main.go @@ -107,5 +107,5 @@ func New(instance instance) (*NetEnv, error) { } type instance interface { - IntelUpdates() *updates.Updates + IntelUpdates() *updates.Updater } diff --git a/service/process/module.go b/service/process/module.go index e2c6eb7f..624b0ba4 100644 --- a/service/process/module.go +++ b/service/process/module.go @@ -70,5 +70,5 @@ func New(instance instance) (*ProcessModule, error) { } type instance interface { - BinaryUpdates() *updates.Updates + BinaryUpdates() *updates.Updater } diff --git a/service/profile/endpoints/endpoints_test.go b/service/profile/endpoints/endpoints_test.go index 93219473..7f29cedc 100644 --- a/service/profile/endpoints/endpoints_test.go +++ b/service/profile/endpoints/endpoints_test.go @@ -23,11 +23,11 @@ type testInstance struct { db *dbmodule.DBModule api *api.API config *config.Config - updates *updates.Updates + updates *updates.Updater geoip *geoip.GeoIP } -func (stub *testInstance) IntelUpdates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updater { return stub.updates } @@ -84,7 +84,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + stub.updates, err = updates.New(stub, "Test Intel", updates.Config{ Directory: installDir, IndexFile: "index.json", }) diff --git a/service/resolver/main_test.go b/service/resolver/main_test.go index bc780e40..048ab4b5 100644 --- a/service/resolver/main_test.go +++ b/service/resolver/main_test.go @@ -22,11 +22,11 @@ type testInstance struct { base *base.Base api *api.API config *config.Config - updates *updates.Updates + updates *updates.Updater netenv *netenv.NetEnv } -func (stub *testInstance) IntelUpdates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updater { return stub.updates } @@ -99,7 +99,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create netenv: %w", err) } - stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + stub.updates, err = updates.New(stub, "Test Intel", updates.Config{ Directory: installDir, IndexFile: "index.json", }) diff --git a/service/ui/module.go b/service/ui/module.go index 2e4f1d9a..805d0c09 100644 --- a/service/ui/module.go +++ b/service/ui/module.go @@ -82,5 +82,5 @@ func New(instance instance) (*UI, error) { type instance interface { API() *api.API - BinaryUpdates() *updates.Updates + BinaryUpdates() *updates.Updater } diff --git a/service/updates/bundle.go b/service/updates/bundle.go deleted file mode 100644 index deee6666..00000000 --- a/service/updates/bundle.go +++ /dev/null @@ -1,142 +0,0 @@ -package updates - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "time" -) - -const MaxUnpackSize = 1 << 30 // 2^30 == 1GB - -const currentPlatform = runtime.GOOS + "_" + runtime.GOARCH - -type Artifact struct { - Filename string `json:"Filename"` - SHA256 string `json:"SHA256"` - URLs []string `json:"URLs"` - Platform string `json:"Platform,omitempty"` - Unpack string `json:"Unpack,omitempty"` - Version string `json:"Version,omitempty"` - - localFile string -} - -func (a *Artifact) GetFileMode() os.FileMode { - // Special case for portmaster ui. Should be able to be executed from the regular user - if a.Platform == currentPlatform && a.Filename == "portmaster" { - return executableUIFileMode - } - - if a.Platform == currentPlatform { - return executableFileMode - } - - return defaultFileMode -} - -type Bundle struct { - Name string `json:"Bundle"` - Version string `json:"Version"` - Published time.Time `json:"Published"` - Artifacts []Artifact `json:"Artifacts"` -} - -// LoadBundle loads and parses a bundle from filepath. -func LoadBundle(indexFilepath string) (*Bundle, error) { - // Read - content, err := os.ReadFile(indexFilepath) - if err != nil { - return nil, fmt.Errorf("failed to read index file: %w", err) - } - - // Parse - var bundle Bundle - err = json.Unmarshal(content, &bundle) - if err != nil { - return nil, fmt.Errorf("%s %w", indexFilepath, err) - } - - // Filter artifacts - filtered := make([]Artifact, 0) - for _, a := range bundle.Artifacts { - if a.Platform == "" || a.Platform == currentPlatform { - filtered = append(filtered, a) - } - } - bundle.Artifacts = filtered - - return &bundle, nil -} - -// ParseBundle parses a bundle from json string. -func ParseBundle(jsonContent string) (*Bundle, error) { - // Parse - var bundle Bundle - err := json.Unmarshal([]byte(jsonContent), &bundle) - if err != nil { - return nil, fmt.Errorf("failed to parse bundle: %w", err) - } - - // Filter artifacts - filtered := make([]Artifact, 0) - for _, a := range bundle.Artifacts { - if a.Platform == "" || a.Platform == currentPlatform { - filtered = append(filtered, a) - } - } - bundle.Artifacts = filtered - - return &bundle, nil -} - -// Verify checks if the files are present int the dataDir and have the correct hash. -func (bundle *Bundle) Verify(dir string) error { - for _, artifact := range bundle.Artifacts { - artifactPath := filepath.Join(dir, artifact.Filename) - isValid, err := checkIfFileIsValid(artifactPath, artifact) - if err != nil { - return err - } - - if !isValid { - return fmt.Errorf("file is not valid: %s", artifact.Filename) - } - } - - return nil -} - -func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { - // Check if file already exists - file, err := os.Open(filename) - if err != nil { - return false, err - } - defer func() { _ = file.Close() }() - - providedHash, err := hex.DecodeString(artifact.SHA256) - if err != nil { - return false, fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) - } - if len(providedHash) != sha256.Size { - return false, fmt.Errorf("invalid hash length for %s", artifact.SHA256) - } - - // Calculate hash of the file - fileHash := sha256.New() - if _, err := io.Copy(fileHash, file); err != nil { - return false, fmt.Errorf("failed to read file: %w", err) - } - hashInBytes := fileHash.Sum(nil) - if !bytes.Equal(providedHash, hashInBytes) { - return false, fmt.Errorf("file exist but the hash does not match: %s", filename) - } - return true, nil -} diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 84259901..a8621271 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -10,275 +10,210 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "os" "path/filepath" - "strings" - - semver "github.com/hashicorp/go-version" "github.com/safing/portmaster/base/log" ) type Downloader struct { - dir string - indexFile string + u *Updater + index *Index indexURLs []string - bundle *Bundle - version *semver.Version + + existingFiles map[string]string httpClient http.Client } -func CreateDownloader(index UpdateIndex) Downloader { - return Downloader{ - dir: index.DownloadDirectory, - indexFile: index.IndexFile, - indexURLs: index.IndexURLs, +func NewDownloader(u *Updater, indexURLs []string) *Downloader { + return &Downloader{ + u: u, + indexURLs: indexURLs, } } -func (d *Downloader) downloadIndexFile(ctx context.Context) error { - // Make sure dir exists - err := os.MkdirAll(d.dir, defaultDirMode) +func (d *Downloader) updateIndex(ctx context.Context) error { + // Make sure dir exists. + err := os.MkdirAll(d.u.cfg.DownloadDirectory, defaultDirMode) if err != nil { - return fmt.Errorf("failed to create directory for updates: %s", d.dir) + return fmt.Errorf("create download directory: %s", d.u.cfg.DownloadDirectory) } - var content string + + // Try to download the index from one of the index URLs. + var ( + indexData []byte + index *Index + ) for _, url := range d.indexURLs { - content, err = d.downloadIndexFileFromURL(ctx, url) - if err != nil { - log.Warningf("updates: failed while downloading index file: %s", err) - continue - } - // Downloading was successful. - var bundle *Bundle - bundle, err = ParseBundle(content) - if err != nil { - log.Warningf("updates: %s", err) - continue - } - // Parsing was successful - var version *semver.Version - version, err = semver.NewVersion(bundle.Version) - if err != nil { - log.Warningf("updates: failed to parse bundle version: %s", err) - continue + // Download and verify index. + indexData, index, err = d.getIndex(ctx, url) + if err == nil { + // Valid index found! + break } - // All checks passed. Set and exit the loop. - d.bundle = bundle - d.version = version - err = nil - break + log.Warningf("updates: failed to update index from %q: %s", url, err) + err = fmt.Errorf("update index file from %q: %s", url, err) } - if err != nil { - return err + return fmt.Errorf("all index URLs failed, last error: %w", err) } + d.index = index - // Write the content into a file. - indexFilepath := filepath.Join(d.dir, d.indexFile) - err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode) + // Write the index into a file. + indexFilepath := filepath.Join(d.u.cfg.DownloadDirectory, d.u.cfg.IndexFile) + err = os.WriteFile(indexFilepath, []byte(indexData), defaultFileMode) if err != nil { - return fmt.Errorf("failed to write index file: %w", err) + return fmt.Errorf("write index file: %w", err) } return nil } -// Verify verifies if the downloaded files match the corresponding hash. -func (d *Downloader) Verify() error { - err := d.parseBundle() +func (d *Downloader) getIndex(ctx context.Context, url string) (indexData []byte, bundle *Index, err error) { + // Download data from URL. + indexData, err = d.downloadData(ctx, url) if err != nil { - return err + return nil, nil, fmt.Errorf("GET index: %w", err) } - return d.bundle.Verify(d.dir) + // Verify and parse index. + bundle, err = ParseIndex(indexData, d.u.cfg.Verify) + if err != nil { + return nil, nil, fmt.Errorf("parse index: %w", err) + } + + return indexData, bundle, nil } -func (d *Downloader) parseBundle() error { - indexFilepath := filepath.Join(d.dir, d.indexFile) - var err error - d.bundle, err = LoadBundle(indexFilepath) - if err != nil { - return err +// gatherExistingFiles gathers the checksums on existing files. +func (d *Downloader) gatherExistingFiles(dir string) error { + // Make sure map is initialized. + if d.existingFiles == nil { + d.existingFiles = make(map[string]string) } - d.version, err = semver.NewVersion(d.bundle.Version) - if err != nil { - return err - } - return nil -} - -func (d *Downloader) downloadIndexFileFromURL(ctx context.Context, url string) (string, error) { - // Request the index file - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) - if err != nil { - return "", fmt.Errorf("failed to create GET request to: %w", err) - } - if UserAgent != "" { - req.Header.Set("User-Agent", UserAgent) - } - - // Perform request - resp, err := d.httpClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed GET request to %s: %w", url, err) - } - defer func() { _ = resp.Body.Close() }() - - // Check the status code - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return "", fmt.Errorf("received error from the server status code: %s", resp.Status) - } - - // Read the content. - content, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - return string(content), nil -} - -// CopyMatchingFilesFromCurrent check if there the current bundle files has matching files with the new bundle and copies them if they match. -func (d *Downloader) copyMatchingFilesFromCurrent(currentFiles map[string]File) error { - // Make sure new dir exists - _ = os.MkdirAll(d.dir, defaultDirMode) - - for _, a := range d.bundle.Artifacts { - currentFile, ok := currentFiles[a.Filename] - if ok && currentFile.Sha256() == a.SHA256 { - // Read the content of the current file. - content, err := os.ReadFile(currentFile.Path()) - if err != nil { - return fmt.Errorf("failed to read file %s: %w", currentFile.Path(), err) - } - - // Check if the content matches the artifact hash - expectedHash, err := hex.DecodeString(a.SHA256) - if err != nil || len(expectedHash) != sha256.Size { - return fmt.Errorf("invalid artifact hash %s: %w", a.SHA256, err) - } - hash := sha256.Sum256(content) - if !bytes.Equal(expectedHash, hash[:]) { - return fmt.Errorf("expected and file hash mismatch: %s", currentFile.Path()) - } - - // Create new file - destFilePath := filepath.Join(d.dir, a.Filename) - err = os.WriteFile(destFilePath, content, a.GetFileMode()) - if err != nil { - return fmt.Errorf("failed to write to file %s: %w", destFilePath, err) - } - log.Debugf("updates: file copied from current version: %s", a.Filename) - } - } - return nil -} - -func (d *Downloader) downloadAndVerify(ctx context.Context) error { - // Make sure we have the bundle file parsed. - err := d.parseBundle() - if err != nil { - return fmt.Errorf("invalid update bundle file: %w", err) - } - - // Make sure dir exists - _ = os.MkdirAll(d.dir, defaultDirMode) - - for _, artifact := range d.bundle.Artifacts { - filePath := filepath.Join(d.dir, artifact.Filename) - - // Check file is already downloaded and valid. - exists, _ := checkIfFileIsValid(filePath, artifact) - if exists { - log.Debugf("updates: file already downloaded: %s", filePath) - continue - } - - // Download artifact - err := d.processArtifact(ctx, artifact, filePath) + // Walk directory, just log errors. + err := filepath.WalkDir(dir, func(fullpath string, entry fs.DirEntry, err error) error { + // Fail on access error. if err != nil { return err } - } - return nil -} -func (d *Downloader) processArtifact(ctx context.Context, artifact Artifact, filePath string) error { - providedHash, err := hex.DecodeString(artifact.SHA256) - if err != nil || len(providedHash) != sha256.Size { - return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) - } + // Skip folders. + if entry.IsDir() { + return nil + } - // Download and verify - log.Debugf("updates: downloading file: %s", artifact.Filename) - content, err := d.downloadAndVerifyArtifact(ctx, artifact.URLs, artifact.Unpack, providedHash) - if err != nil { - return fmt.Errorf("failed to download artifact: %w", err) - } - - // Save - tmpFilename := fmt.Sprintf("%s.download", filePath) - err = os.WriteFile(tmpFilename, content, artifact.GetFileMode()) - if err != nil { - return fmt.Errorf("failed to write to file: %w", err) - } - - // Rename - err = os.Rename(tmpFilename, filePath) - if err != nil { - return fmt.Errorf("failed to rename file: %w", err) - } - - log.Infof("updates: file downloaded and verified: %s", artifact.Filename) - - return nil -} - -func (d *Downloader) downloadAndVerifyArtifact(ctx context.Context, urls []string, unpack string, expectedHash []byte) ([]byte, error) { - var err error - var content []byte - - for _, url := range urls { - // Download - content, err = d.downloadFile(ctx, url) + // Read full file. + fileData, err := os.ReadFile(fullpath) if err != nil { - err := fmt.Errorf("failed to download artifact from url: %s, %w", url, err) - log.Warningf("%s", err) - continue + log.Debugf("updates: failed to read file %q while searching for existing files: %w", fullpath, err) + return fmt.Errorf("failed to read file %s: %w", fullpath, err) } - // Decompress - if unpack != "" { - content, err = decompress(unpack, content) - if err != nil { - err = fmt.Errorf("failed to decompress artifact: %w", err) - log.Warningf("%s", err) - continue - } - } + // Calculate checksum and add it to the existing files. + hashSum := sha256.Sum256(fileData) + d.existingFiles[hex.EncodeToString(hashSum[:])] = fullpath - // Calculate and verify hash - hash := sha256.Sum256(content) - if !bytes.Equal(expectedHash, hash[:]) { - err := fmt.Errorf("artifact hash does not match") - log.Warningf("%s", err) - continue - } - - // All file downloaded and verified. - return content, nil + return nil + }) + if err != nil { + return fmt.Errorf("searching for existing files: %w", err) } - return nil, err + return nil } -func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, error) { - // Try to make the request +func (d *Downloader) downloadArtifacts(ctx context.Context) error { + // Make sure dir exists. + err := os.MkdirAll(d.u.cfg.DownloadDirectory, defaultDirMode) + if err != nil { + return fmt.Errorf("create download directory: %s", d.u.cfg.DownloadDirectory) + } + +artifacts: + for _, artifact := range d.index.Artifacts { + dstFilePath := filepath.Join(d.u.cfg.DownloadDirectory, artifact.Filename) + + // Check if we can copy the artifact from disk instead. + if existingFile, ok := d.existingFiles[artifact.SHA256]; ok { + // Check if this is the same file. + if existingFile == dstFilePath { + continue artifacts + } + // Copy and check. + err = copyAndCheckSHA256Sum(existingFile, dstFilePath, artifact.SHA256, artifact.GetFileMode()) + if err == nil { + continue artifacts + } + log.Debugf("updates: failed to copy existing file %s: %w", artifact.Filename, err) + } + + // Try to download the artifact from one of the URLs. + var artifactData []byte + artifactURLs: + for _, url := range artifact.URLs { + // Download and verify index. + artifactData, err = d.getArtifact(ctx, artifact, url) + if err == nil { + // Valid artifact found! + break artifactURLs + } + err = fmt.Errorf("update index file from %q: %s", url, err) + } + if err != nil { + return fmt.Errorf("all artifact URLs for %s failed, last error: %w", artifact.Filename, err) + } + + // Write artifact to temporary file. + tmpFilename := dstFilePath + ".download" + err = os.WriteFile(tmpFilename, artifactData, artifact.GetFileMode()) + if err != nil { + return fmt.Errorf("write %s to temp file: %w", artifact.Filename, err) + } + + // Rename/Move to actual location. + err = os.Rename(tmpFilename, dstFilePath) + if err != nil { + return fmt.Errorf("rename %s after write: %w", artifact.Filename, err) + } + + log.Infof("updates: downloaded and verified %s", artifact.Filename) + } + return nil +} + +func (d *Downloader) getArtifact(ctx context.Context, artifact Artifact, url string) ([]byte, error) { + // Download data from URL. + artifactData, err := d.downloadData(ctx, url) + if err != nil { + return nil, fmt.Errorf("GET artifact: %w", err) + } + + // Decompress artifact data, if configured. + // TODO: Normally we should do operations on "untrusted" data _after_ verification, + // but we really want the checksum to be for the unpacked data. Should we add another checksum, or is HTTPS enough? + if artifact.Unpack != "" { + artifactData, err = decompress(artifact.Unpack, artifactData) + if err != nil { + return nil, fmt.Errorf("decompress: %w", err) + } + } + + // Verify checksum. + if err := checkSHA256Sum(artifactData, artifact.SHA256); err != nil { + return nil, err + } + + return artifactData, nil +} + +func (d *Downloader) downloadData(ctx context.Context, url string) ([]byte, error) { + // Setup request. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { return nil, fmt.Errorf("failed to create GET request to %s: %w", url, err) @@ -286,17 +221,20 @@ func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, erro if UserAgent != "" { req.Header.Set("User-Agent", UserAgent) } + + // Start request with shared http client. resp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed a get file request to: %w", err) } defer func() { _ = resp.Body.Close() }() - // Check if the server returned an error + // Check for HTTP status errors. if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status) } + // Read the full body and return it. content, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read body of response: %w", err) @@ -304,25 +242,6 @@ func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, erro return content, nil } -func (d *Downloader) deleteUnfinishedDownloads() error { - entries, err := os.ReadDir(d.dir) - if err != nil { - return err - } - for _, e := range entries { - // Check if the current file has the download extension - if !e.IsDir() && strings.HasSuffix(e.Name(), ".download") { - path := filepath.Join(d.dir, e.Name()) - log.Warningf("updates: deleting unfinished download file: %s\n", path) - err := os.Remove(path) - if err != nil { - log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err) - } - } - } - return nil -} - func decompress(cType string, fileBytes []byte) ([]byte, error) { switch cType { case "zip": @@ -335,46 +254,48 @@ func decompress(cType string, fileBytes []byte) ([]byte, error) { } func decompressGzip(data []byte) ([]byte, error) { - // Create a gzip reader from the byte array + // Create a gzip reader from the byte slice. gzipReader, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("failed to create gzip reader: %w", err) + return nil, fmt.Errorf("create gzip reader: %w", err) } defer func() { _ = gzipReader.Close() }() + // Copy from the gzip reader into a new buffer. var buf bytes.Buffer _, err = io.CopyN(&buf, gzipReader, MaxUnpackSize) if err != nil && !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("failed to read gzip file: %w", err) + return nil, fmt.Errorf("read gzip file: %w", err) } return buf.Bytes(), nil } func decompressZip(data []byte) ([]byte, error) { - // Create a zip reader from the byte array + // Create a zip reader from the byte slice. zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { - return nil, fmt.Errorf("failed to create zip reader: %w", err) + return nil, fmt.Errorf("create zip reader: %w", err) } - // Ensure there is only one file in the zip + // Ensure there is only one file in the zip. if len(zipReader.File) != 1 { return nil, fmt.Errorf("zip file must contain exactly one file") } - // Read the single file in the zip + // Open single file in the zip. file := zipReader.File[0] fileReader, err := file.Open() if err != nil { - return nil, fmt.Errorf("failed to open file in zip: %w", err) + return nil, fmt.Errorf("open file in zip: %w", err) } defer func() { _ = fileReader.Close() }() + // Copy from the zip reader into a new buffer. var buf bytes.Buffer _, err = io.CopyN(&buf, fileReader, MaxUnpackSize) if err != nil && !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("failed to read file in zip: %w", err) + return nil, fmt.Errorf("read file in zip: %w", err) } return buf.Bytes(), nil diff --git a/service/updates/index.go b/service/updates/index.go new file mode 100644 index 00000000..77b58845 --- /dev/null +++ b/service/updates/index.go @@ -0,0 +1,295 @@ +package updates + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "time" + + semver "github.com/hashicorp/go-version" + "github.com/safing/jess" + "github.com/safing/jess/filesig" +) + +// MaxUnpackSize defines the maximum size that is allowed to be unpacked. +const MaxUnpackSize = 1 << 30 // 2^30 == 1GB + +const currentPlatform = runtime.GOOS + "_" + runtime.GOARCH + +var zeroVersion = semver.Must(semver.NewVersion("0.0.0")) + +// Artifacts represents a single file with metadata. +type Artifact struct { + Filename string `json:"Filename"` + SHA256 string `json:"SHA256"` + URLs []string `json:"URLs"` + Platform string `json:"Platform,omitempty"` + Unpack string `json:"Unpack,omitempty"` + Version string `json:"Version,omitempty"` + + localFile string +} + +// GetFileMode returns the required filesystem permission for the artifact. +func (a *Artifact) GetFileMode() os.FileMode { + // Special case for portmaster ui. Should be able to be executed from the regular user + if a.Platform == currentPlatform && a.Filename == "portmaster" { + return executableUIFileMode + } + + if a.Platform == currentPlatform { + return executableFileMode + } + + return defaultFileMode +} + +// Index represents a collection of artifacts with metadata. +type Index struct { + Name string `json:"Name"` + Version string `json:"Version"` + Published time.Time `json:"Published"` + Artifacts []Artifact `json:"Artifacts"` + + versionNum *semver.Version +} + +// LoadIndex loads and parses an index from the given filename. +func LoadIndex(filename string, trustStore jess.TrustStore) (*Index, error) { + // Read index file from disk. + content, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("read index file: %w", err) + } + + // Parse and return. + return ParseIndex(content, trustStore) +} + +// ParseIndex parses an index from a json string. +func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error) { + // Verify signature. + if trustStore != nil { + if err := filesig.VerifyJSONSignature(jsonContent, trustStore); err != nil { + return nil, fmt.Errorf("verify: %w", err) + } + } + + // Parse json. + var index Index + err := json.Unmarshal([]byte(jsonContent), &index) + if err != nil { + return nil, fmt.Errorf("parse index: %w", err) + } + + // Parse version number, if set. + if index.Version != "" { + versionNum, err := semver.NewVersion(index.Version) + if err != nil { + return nil, fmt.Errorf("invalid index version %q: %w", index.Version, err) + } + index.versionNum = versionNum + } + + // Filter artifacts by currnet platform. + filtered := make([]Artifact, 0) + for _, a := range index.Artifacts { + if a.Platform == "" || a.Platform == currentPlatform { + filtered = append(filtered, a) + } + } + index.Artifacts = filtered + + return &index, nil +} + +// CanDoUpgrades returns whether the index is able to follow a secure upgrade path. +func (index *Index) CanDoUpgrades() error { + switch { + case index.versionNum == nil: + return errors.New("missing version number") + + case index.Published.IsZero(): + return errors.New("missing publish date") + + case index.Published.After(time.Now().Add(15 * time.Minute)): + return fmt.Errorf("is from the future (%s)", time.Until(index.Published).Round(time.Minute)) + + default: + return nil + } +} + +// ShouldUpgradeTo returns whether the given index is a successor and should be upgraded to. +func (index *Index) ShouldUpgradeTo(newIndex *Index) error { + // Check if both indexes can do upgrades. + if err := index.CanDoUpgrades(); err != nil { + return fmt.Errorf("current index cannot do upgrades: %w", err) + } + if err := newIndex.CanDoUpgrades(); err != nil { + return fmt.Errorf("new index cannot do upgrade: %w") + } + + switch { + case index.versionNum.Equal(zeroVersion): + // The zero version is used for bootstrapping. + // Upgrade in any case. + return nil + + case index.Name != newIndex.Name: + return errors.New("index names do not match") + + case index.versionNum.GreaterThan(newIndex.versionNum): + return errors.New("current index has newer version") + + case index.Published.After(newIndex.Published): + return errors.New("current index was published later") + + case index.Published.Equal(newIndex.Published): + // "Do nothing". + return ErrSameIndex + + default: + // Upgrade! + return nil + } +} + +// VerifyArtifacts checks if all artifacts are present in the given dir and have the correct hash. +func (index *Index) VerifyArtifacts(dir string) error { + for _, artifact := range index.Artifacts { + err := checkSHA256SumFile(filepath.Join(dir, artifact.Filename), artifact.SHA256) + if err != nil { + return fmt.Errorf("verify %s: %s", artifact.Filename, err) + } + } + + return nil +} + +func (index *Index) Export(signingKey *jess.Signet, trustStore jess.TrustStore) ([]byte, error) { + // Serialize to json. + indexData, err := json.Marshal(index) + if err != nil { + return nil, fmt.Errorf("serialize: %w", err) + } + + // Do not sign if signing key is not given. + if signingKey == nil { + return indexData, nil + } + + // Make envelope. + envelope := jess.NewUnconfiguredEnvelope() + envelope.SuiteID = jess.SuiteSignV1 + envelope.Senders = []*jess.Signet{signingKey} + + // Sign json data. + signedIndex, err := filesig.AddJSONSignature(indexData, envelope, trustStore) + if err != nil { + return nil, fmt.Errorf("sign: %w", err) + } + + return signedIndex, nil +} + +func checkSHA256SumFile(filename string, sha256sum string) error { + // Check expected hash. + expectedDigest, err := hex.DecodeString(sha256sum) + if err != nil { + return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err) + } + if len(expectedDigest) != sha256.Size { + return fmt.Errorf("invalid size for expected hash %s: %w", sha256sum, err) + } + + // Open file for checking. + file, err := os.Open(filename) + if err != nil { + return fmt.Errorf("open file: %w", err) + } + defer func() { _ = file.Close() }() + + // Calculate hash of the file. + fileHash := sha256.New() + if _, err := io.Copy(fileHash, file); err != nil { + return fmt.Errorf("read file: %w", err) + } + if subtle.ConstantTimeCompare(fileHash.Sum(nil), expectedDigest) != 1 { + return errors.New("sha256sum mismatch") + } + + return nil +} + +func checkSHA256Sum(fileData []byte, sha256sum string) error { + // Check expected hash. + expectedDigest, err := hex.DecodeString(sha256sum) + if err != nil { + return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err) + } + if len(expectedDigest) != sha256.Size { + return fmt.Errorf("invalid size for expected hash %s: %w", sha256sum, err) + } + + // Calculate and compare hash of the file. + hashSum := sha256.Sum256(fileData) + if subtle.ConstantTimeCompare(hashSum[:], expectedDigest) != 1 { + return errors.New("sha256sum mismatch") + } + + return nil +} + +// copyAndCheckSHA256Sum copies the file from src to dst and check the sha256 sum. +// As a special case, if the sha256sum is not given, it is not checked. +func copyAndCheckSHA256Sum(src, dst, sha256sum string, fileMode fs.FileMode) error { + // Check expected hash. + var expectedDigest []byte + if sha256sum != "" { + expectedDigest, err := hex.DecodeString(sha256sum) + if err != nil { + return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err) + } + if len(expectedDigest) != sha256.Size { + return fmt.Errorf("invalid size for expected hash %s: %w", sha256sum, err) + } + } + + // Read file from source. + fileData, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("read src file: %w", err) + } + + // Calculate and compare hash of the file. + if len(expectedDigest) > 0 { + hashSum := sha256.Sum256(fileData) + if subtle.ConstantTimeCompare(hashSum[:], expectedDigest) != 1 { + return errors.New("sha256sum mismatch") + } + } + + // Write to temporary file. + tmpDst := dst + ".copy" + err = os.WriteFile(tmpDst, fileData, fileMode) + if err != nil { + return fmt.Errorf("write temp dst file: %w", err) + } + + // Rename/Move to actual location. + err = os.Rename(tmpDst, dst) + if err != nil { + return fmt.Errorf("rename dst file after write: %w", err) + } + + return nil +} diff --git a/service/updates/bundlegeneration.go b/service/updates/index_scan.go similarity index 84% rename from service/updates/bundlegeneration.go rename to service/updates/index_scan.go index 45871edc..aa61417e 100644 --- a/service/updates/bundlegeneration.go +++ b/service/updates/index_scan.go @@ -19,7 +19,7 @@ import ( semver "github.com/hashicorp/go-version" ) -type BundleFileSettings struct { +type IndexScanConfig struct { Name string Version string PrimaryArtifact string @@ -34,7 +34,7 @@ type BundleFileSettings struct { unpackFilesGlobs map[string]glob.Glob } -func (bs *BundleFileSettings) init() error { +func (bs *IndexScanConfig) init() error { // Transform base URL into expected format. bs.cleanedBaseURL = strings.TrimSuffix(bs.BaseURL, "/") + "/" @@ -62,7 +62,7 @@ func (bs *BundleFileSettings) init() error { } // IsIgnored returns whether a filename should be ignored. -func (bs *BundleFileSettings) IsIgnored(filename string) bool { +func (bs *IndexScanConfig) IsIgnored(filename string) bool { for _, ignoreGlob := range bs.ignoreFilesGlobs { if ignoreGlob.Match(filename) { return true @@ -73,7 +73,7 @@ func (bs *BundleFileSettings) IsIgnored(filename string) bool { } // UnpackSetting returns the unpack setings for the given filename. -func (bs *BundleFileSettings) UnpackSetting(filename string) (string, error) { +func (bs *IndexScanConfig) UnpackSetting(filename string) (string, error) { var foundSetting string settings: @@ -94,21 +94,21 @@ settings: return foundSetting, nil } -// GenerateBundleFromDir generates a bundle from a given folder. -func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bundle, error) { +// GenerateIndexFromDir generates a index from a given folder. +func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) { artifacts := make(map[string]Artifact) // Initialize. - err := settings.init() + err := cfg.init() if err != nil { - return nil, fmt.Errorf("invalid bundle settings: %w", err) + return nil, fmt.Errorf("invalid index scan config: %w", err) } - bundleDir, err = filepath.Abs(bundleDir) + sourceDir, err = filepath.Abs(sourceDir) if err != nil { - return nil, fmt.Errorf("invalid bundle dir: %w", err) + return nil, fmt.Errorf("invalid index dir: %w", err) } - err = filepath.WalkDir(bundleDir, func(fullpath string, d fs.DirEntry, err error) error { + err = filepath.WalkDir(sourceDir, func(fullpath string, d fs.DirEntry, err error) error { // Fail on access error. if err != nil { return err @@ -122,13 +122,13 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund } // Get relative path for processing. - relpath, err := filepath.Rel(bundleDir, fullpath) + relpath, err := filepath.Rel(sourceDir, fullpath) if err != nil { return fmt.Errorf("invalid relative path for %s: %w", fullpath, err) } // Check if file is in the ignore list. - if settings.IsIgnored(relpath) { + if cfg.IsIgnored(relpath) { return nil } @@ -184,7 +184,7 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund artifact := Artifact{} // Check if the caller provided a template for the artifact. - if t, ok := settings.Templates[identifier]; ok { + if t, ok := cfg.Templates[identifier]; ok { artifact = t } @@ -192,14 +192,14 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund if artifact.Filename == "" { artifact.Filename = identifier } - if len(artifact.URLs) == 0 && settings.BaseURL != "" { - artifact.URLs = []string{settings.cleanedBaseURL + relpath} + if len(artifact.URLs) == 0 && cfg.BaseURL != "" { + artifact.URLs = []string{cfg.cleanedBaseURL + relpath} } if artifact.Platform == "" { artifact.Platform = platform } if artifact.Unpack == "" { - unpackSetting, err := settings.UnpackSetting(relpath) + unpackSetting, err := cfg.UnpackSetting(relpath) if err != nil { return fmt.Errorf("invalid unpack setting for %s at %s: %w", key, relpath, err) } @@ -225,20 +225,20 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund return nil, fmt.Errorf("scanning dir: %w", err) } - // Create base bundle. - bundle := &Bundle{ - Name: settings.Name, - Version: settings.Version, + // Create base index. + index := &Index{ + Name: cfg.Name, + Version: cfg.Version, Published: time.Now(), } - if bundle.Version == "" && settings.PrimaryArtifact != "" { - pv, ok := artifacts[settings.PrimaryArtifact] + if index.Version == "" && cfg.PrimaryArtifact != "" { + pv, ok := artifacts[cfg.PrimaryArtifact] if ok { - bundle.Version = pv.Version + index.Version = pv.Version } } - if bundle.Name == "" { - bundle.Name = strings.Trim(filepath.Base(bundleDir), "./\\") + if index.Name == "" { + index.Name = strings.Trim(filepath.Base(sourceDir), "./\\") } // Convert to slice and compute hashes. @@ -257,7 +257,7 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund } // Remove default versions. - if artifact.Version == bundle.Version { + if artifact.Version == index.Version { artifact.Version = "" } @@ -282,8 +282,8 @@ func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bund }) // Assign and return. - bundle.Artifacts = export - return bundle, nil + index.Artifacts = export + return index, nil } func selectLatestArtifacts(artifacts []Artifact) ([]Artifact, error) { @@ -373,8 +373,8 @@ func getIdentifierAndVersion(versionedPath string) (identifier, version string, return dirPath + filename, version, true } -// GenerateMockFolder generates mock bundle folder for testing. -func GenerateMockFolder(dir, name, version string) error { +// GenerateMockFolder generates mock index folder for testing. +func GenerateMockFolder(dir, name, version string) error { // FIXME: move this to test? // Make sure dir exists _ = os.MkdirAll(dir, defaultDirMode) @@ -400,7 +400,7 @@ func GenerateMockFolder(dir, name, version string) error { } _ = file.Close() - bundle, err := GenerateBundleFromDir(dir, BundleFileSettings{ + index, err := GenerateIndexFromDir(dir, IndexScanConfig{ Name: name, Version: version, }) @@ -408,12 +408,12 @@ func GenerateMockFolder(dir, name, version string) error { return err } - bundleStr, err := json.MarshalIndent(bundle, "", " ") + indexJson, err := json.MarshalIndent(index, "", " ") if err != nil { - fmt.Fprintf(os.Stderr, "failed to marshal bundle: %s\n", err) + fmt.Fprintf(os.Stderr, "failed to marshal index: %s\n", err) } - err = os.WriteFile(filepath.Join(dir, "index.json"), bundleStr, defaultFileMode) + err = os.WriteFile(filepath.Join(dir, "index.json"), indexJson, defaultFileMode) if err != nil { return err } diff --git a/service/updates/module.go b/service/updates/module.go index f33c830c..3cb183a7 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -3,9 +3,14 @@ package updates import ( "errors" "fmt" + "os" + "path/filepath" "runtime" + "strings" + "sync" "time" + "github.com/safing/jess" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" @@ -14,6 +19,7 @@ import ( const ( updateTaskRepeatDuration = 1 * time.Hour + noNewUpdateNotificationID = "updates:no-new-update" updateAvailableNotificationID = "updates:update-available" updateFailedNotificationID = "updates:update-failed" corruptInstallationNotificationID = "updates:corrupt-installation" @@ -23,43 +29,100 @@ const ( ResourceUpdateEvent = "resource update" ) -var ( - // UserAgent is an HTTP User-Agent that is used to add - // more context to requests made by the registry when - // fetching resources from the update server. - UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) +// UserAgent is an HTTP User-Agent that is used to add +// more context to requests made by the registry when +// fetching resources from the update server. +var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) - ErrNotFound error = errors.New("file not found") +// Errors. +var ( + ErrNotFound = errors.New("file not found") + ErrSameIndex = errors.New("same index") + + ErrNoUpdateAvailable = errors.New("no update available") + ErrActionRequired = errors.New("action required") ) -// UpdateIndex holds the configuration for the updates module. -type UpdateIndex struct { - Directory string +// Config holds the configuration for the updates module. +type Config struct { + // Directory is the main directory where the currently to-be-used artifacts live. + Directory string + // DownloadDirectory is the directory where new artifacts are downloaded to and prepared for upgrading. + // After the upgrade, this directory is cleared. DownloadDirectory string - PurgeDirectory string - Ignore []string - IndexURLs []string - IndexFile string - AutoApply bool - NeedsRestart bool + // PurgeDirectory is the directory where old artifacts are moved to during the upgrade process. + // After the upgrade, this directory is cleared. + PurgeDirectory string + // Ignore defines file and directory names within the main directory that should be ignored during the upgrade. + Ignore []string + + // IndexURLs defines file + IndexURLs []string + // IndexFile is the name of the index file used in the directories. + IndexFile string + // Verify enables and specifies the trust the index signatures will be checked against. + Verify jess.TrustStore + + // AutoDownload defines that updates may be downloaded automatically without outside trigger. + AutoDownload bool + // AutoApply defines that updates may be automatically applied without outside trigger. + // Requires AutoDownload the be enabled. + AutoApply bool + // NeedsRestart defines that a restart is required after an upgrade has been completed. + // Restart is triggered automatically, if Notify is disabled. + NeedsRestart bool + // Notify defines whether the user shall be informed about events via notifications. + // If enabled, disables automatic restart after upgrade. + Notify bool } -// Updates provides access to released artifacts. -type Updates struct { +// Check looks for obvious configuration errors. +func (cfg *Config) Check() error { + // Check if required fields are set. + switch { + case cfg.Directory == "": + return errors.New("directory must be set") + case cfg.DownloadDirectory == "": + return errors.New("download directory must be set") + case cfg.PurgeDirectory == "": + return errors.New("purge directory must be set") + case cfg.IndexFile == "": + return errors.New("index file must be set") + case cfg.AutoApply && !cfg.AutoDownload: + return errors.New("auto apply is set, but auto download is not") + } + + // Check if Ignore contains paths. + for i, s := range cfg.Ignore { + if strings.ContainsRune(s, filepath.Separator) { + return fmt.Errorf("ignore entry #%d invalid: must be file or directory name, not path", i+1) + } + } + + // Check if IndexURLs are HTTPS. + for i, url := range cfg.IndexURLs { + if !strings.HasPrefix(url, "https://") { + return fmt.Errorf("index URL #%d invalid: is not a HTTPS url", i+1) + } + } + + return nil +} + +// Updater provides access to released artifacts. +type Updater struct { m *mgr.Manager states *mgr.StateMgr + cfg Config + + index *Index + indexLock sync.Mutex updateCheckWorkerMgr *mgr.WorkerMgr upgradeWorkerMgr *mgr.WorkerMgr EventResourcesUpdated *mgr.EventMgr[struct{}] - registry Registry - downloader Downloader - - autoApply bool - needsRestart bool - corruptedInstallation bool isUpdateRunning *abool.AtomicBool @@ -68,224 +131,280 @@ type Updates struct { } // New returns a new Updates module. -func New(instance instance, name string, index UpdateIndex) (*Updates, error) { +func New(instance instance, name string, cfg Config) (*Updater, error) { m := mgr.New(name) - module := &Updates{ + module := &Updater{ m: m, states: m.NewStateMgr(), + cfg: cfg, EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), - autoApply: index.AutoApply, - needsRestart: index.NeedsRestart, isUpdateRunning: abool.NewBool(false), instance: instance, } - // Workers - module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil).Repeat(updateTaskRepeatDuration) - module.upgradeWorkerMgr = m.NewWorkerMgr("upgrader", func(w *mgr.WorkerCtx) error { - if !module.isUpdateRunning.SetToIf(false, true) { - return fmt.Errorf("unable to apply updates, concurrent updater task is running") - } - // Make sure to unset it - defer module.isUpdateRunning.UnSet() - - module.applyUpdates(module.downloader, false) - return nil - }, nil) - - var err error - module.registry, err = CreateRegistry(index) - if err != nil { - // Installation is corrupt, set flag and fall back to folder scanning for artifacts discovery. - log.Criticalf("updates: failed to create registry: %s (falling back to folder scanning)", err) - module.corruptedInstallation = true - - module.registry, err = CreateRegistryFromFolder(index) - if err != nil { - return nil, err - } + // Check config. + if err := module.cfg.Check(); err != nil { + return nil, fmt.Errorf("config is invalid: %w", err) } - module.downloader = CreateDownloader(index) + // Create Workers. + module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.updateCheckWorker, nil). + Repeat(updateTaskRepeatDuration) + module.upgradeWorkerMgr = m.NewWorkerMgr("upgrader", module.upgradeWorker, nil) + + // Load index. + index, err := LoadIndex(filepath.Join(cfg.Directory, cfg.IndexFile), cfg.Verify) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + log.Errorf("updates: invalid index file, falling back to dir scan: %w", err) + } + + // Fall back to scanning the directory. + index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"}) + if err != nil { + return nil, fmt.Errorf("updates index load and dir scan failed: %w", err) + } + } + module.index = index return module, nil } -func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error { +func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreVersion, forceApply bool) (err error) { + // Make sure only one update process is running. if !u.isUpdateRunning.SetToIf(false, true) { - return fmt.Errorf("unable to check for updates, concurrent updater task is running") + return fmt.Errorf("an updater task is already running, please try again later") } - // Make sure to unset it on return. defer u.isUpdateRunning.UnSet() - // Download the index file. - err := u.downloader.downloadIndexFile(wc.Ctx()) - if err != nil { - return fmt.Errorf("failed to download index file: %w", err) + // FIXME: Switch to mutex? + + // Create a new downloader. + downloader := NewDownloader(u, indexURLs) + + // Update or load the index file. + if len(indexURLs) > 0 { + // Download fresh copy, if indexURLs are given. + err = downloader.updateIndex(w.Ctx()) + if err != nil { + return fmt.Errorf("update index file: %w", err) + } + } else { + // Otherwise, load index from download dir. + downloader.index, err = LoadIndex(filepath.Join(u.cfg.Directory, u.cfg.IndexFile), u.cfg.Verify) + if err != nil { + return fmt.Errorf("load previously downloaded index file: %w", err) + } } // Check if there is a new version. - if u.downloader.version.LessThanOrEqual(u.registry.version) { - log.Infof("updates: check compete: no new updates") + if !ignoreVersion { + // Get index to check version. + u.indexLock.Lock() + index := u.index + u.indexLock.Unlock() + // Check with local pointer to index. + if err := index.ShouldUpgradeTo(downloader.index); err != nil { + log.Infof("updates: no new or eligible update: %s", err) + if u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().NotifyInfo( + noNewUpdateNotificationID, + "No Updates Available", + "Portmaster v"+u.index.Version+" is the newest version.", + ) + } + return ErrNoUpdateAvailable + } + } + + // Check if automatic downloads are enabled. + if !u.cfg.AutoDownload && !forceApply { + if u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().NotifyInfo( + updateAvailableNotificationID, + "New Update", + "Portmaster v"+downloader.index.Version+" is available. Click Upgrade to download and upgrade now.", + notifications.Action{ + ID: "upgrade", + Text: "Upgrade Now", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, + }, + ) + } + return fmt.Errorf("%w: apply updates to download and upgrade", ErrActionRequired) + } + + // Check for existing resources before starting to download. + _ = downloader.gatherExistingFiles(u.cfg.Directory) // Artifacts are re-used between versions. + _ = downloader.gatherExistingFiles(u.cfg.DownloadDirectory) // Previous download may have been interrupted. + _ = downloader.gatherExistingFiles(u.cfg.PurgeDirectory) // Revover faster from a failed upgrade. + + // Download any remaining needed files. + // If everything is already found in the download directory, then this is a no-op. + log.Infof("updates: downloading new version: %s %s", downloader.index.Name, downloader.index.Version) + err = downloader.downloadArtifacts(w.Ctx()) + if err != nil { + log.Errorf("updates: failed to download update: %s", err) + if err := u.deleteUnfinishedFiles(u.cfg.DownloadDirectory); err != nil { + log.Debugf("updates: failed to delete unfinished files in download directory %s", u.cfg.DownloadDirectory) + } + return fmt.Errorf("downloading failed: %w", err) + } + + // Notify the user that an upgrade is available. + if !u.cfg.AutoApply && !forceApply { + if u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().NotifyInfo( + updateAvailableNotificationID, + "New Update", + "Portmaster v"+downloader.index.Version+" is available. Click Upgrade to upgrade now.", + notifications.Action{ + ID: "upgrade", + Text: "Upgrade Now", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, + }, + ) + } + return fmt.Errorf("%w: apply updates to download and upgrade", ErrActionRequired) + } + + // Run upgrade procedure. + err = u.upgrade(downloader, ignoreVersion) + if err != nil { + if err := u.deleteUnfinishedFiles(u.cfg.PurgeDirectory); err != nil { + log.Debugf("updates: failed to delete unfinished files in purge directory %s", u.cfg.PurgeDirectory) + } + return err + } + + // Install is complete! + + // Clean up and notify modules of changed files. + u.cleanupAfterUpgrade() + u.EventResourcesUpdated.Submit(struct{}{}) + + // If no restart is needed, we are done. + if !u.cfg.NeedsRestart { return nil } - // Download the new version. - downloadBundle := u.downloader.bundle - log.Infof("updates: check complete: downloading new version: %s %s", downloadBundle.Name, downloadBundle.Version) - err = u.downloader.copyMatchingFilesFromCurrent(u.registry.files) - if err != nil { - log.Warningf("updates: failed to copy files from current installation: %s", err) - } - err = u.downloader.downloadAndVerify(wc.Ctx()) - if err != nil { - log.Errorf("updates: failed to download update: %s", err) - } else { - if u.autoApply { - // Apply updates. - u.applyUpdates(u.downloader, false) - } else { - // Notify the user with option to trigger upgrade. - notifications.NotifyPrompt(updateAvailableNotificationID, "New update is available.", fmt.Sprintf("%s %s", downloadBundle.Name, downloadBundle.Version), notifications.Action{ - ID: "apply", - Text: "Apply", + // Notify user that a restart is required. + if u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().NotifyInfo( + updateAvailableNotificationID, + "Restart Required", + "Portmaster v"+downloader.index.Version+" is installed. Restart to use new version.", + notifications.Action{ + ID: "restart", + Text: "Restart Now", Type: notifications.ActionTypeWebhook, Payload: notifications.ActionTypeWebhookPayload{ Method: "POST", - URL: "updates/apply", + URL: "updates/apply", // FIXME }, - }) - } + }, + ) + return fmt.Errorf("%w: restart required", ErrActionRequired) } + + // Otherwise, trigger restart immediately. + u.instance.Restart() + return nil +} + +func (u *Updater) updateCheckWorker(w *mgr.WorkerCtx) error { + _ = u.updateAndUpgrade(w, u.cfg.IndexURLs, false, false) + // FIXME: Handle errors. + return nil +} + +func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error { + _ = u.updateAndUpgrade(w, u.cfg.IndexURLs, false, true) + // FIXME: Handle errors. return nil } // UpdateFromURL installs an update from the provided url. -func (u *Updates) UpdateFromURL(url string) error { - if !u.isUpdateRunning.SetToIf(false, true) { - return fmt.Errorf("unable to upgrade from url, concurrent updater task is running") - } - - u.m.Go("custom-url-downloader", func(w *mgr.WorkerCtx) error { - // Make sure to unset it on return. - defer u.isUpdateRunning.UnSet() - - // Initialize parameters - index := UpdateIndex{ - DownloadDirectory: u.downloader.dir, - IndexURLs: []string{url}, - IndexFile: u.downloader.indexFile, - } - - // Initialize with proper values and download the index file. - downloader := CreateDownloader(index) - err := downloader.downloadIndexFile(w.Ctx()) - if err != nil { - return err - } - - // Start downloading the artifacts - err = downloader.downloadAndVerify(w.Ctx()) - if err != nil { - return err - } - - // Artifacts are downloaded, perform the update. - u.applyUpdates(downloader, true) - +func (u *Updater) UpdateFromURL(url string) error { + u.m.Go("custom update from url", func(w *mgr.WorkerCtx) error { + _ = u.updateAndUpgrade(w, []string{url}, true, true) return nil }) - return nil -} -func (u *Updates) applyUpdates(downloader Downloader, force bool) error { - currentBundle := u.registry.bundle - downloadBundle := downloader.bundle - - if !force && u.registry.version != nil { - if u.downloader.version.LessThanOrEqual(u.registry.version) { - // No new version, silently return. - return nil - } - } - if currentBundle != nil { - log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version) - } - - err := u.registry.performRecoverableUpgrade(downloader.dir, downloader.indexFile) - if err != nil { - // Notify the user that update failed. - notifications.NotifyPrompt(updateFailedNotificationID, "Failed to apply update.", err.Error()) - return fmt.Errorf("updates: failed to apply updates: %w", err) - } - - if u.needsRestart { - // Perform restart. - u.instance.Restart() - } else { - // Update completed and no restart is needed. Submit an event. - u.EventResourcesUpdated.Submit(struct{}{}) - } return nil } // TriggerUpdateCheck triggers an update check. -func (u *Updates) TriggerUpdateCheck() { +func (u *Updater) TriggerUpdateCheck() { u.updateCheckWorkerMgr.Go() } // TriggerApplyUpdates triggers upgrade. -func (u *Updates) TriggerApplyUpdates() { +func (u *Updater) TriggerApplyUpdates() { u.upgradeWorkerMgr.Go() } // States returns the state manager. -func (u *Updates) States() *mgr.StateMgr { +func (u *Updater) States() *mgr.StateMgr { return u.states } // Manager returns the module manager. -func (u *Updates) Manager() *mgr.Manager { +func (u *Updater) Manager() *mgr.Manager { return u.m } // Start starts the module. -func (u *Updates) Start() error { - // Remove old files - u.m.Go("old files cleaner", func(ctx *mgr.WorkerCtx) error { - _ = u.registry.CleanOldFiles() - _ = u.downloader.deleteUnfinishedDownloads() - return nil - }) - - if u.corruptedInstallation { - notifications.NotifyError(corruptInstallationNotificationID, "Corrupted installation. Reinstall the software.", "") +func (u *Updater) Start() error { + if u.corruptedInstallation && u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().NotifyError( + corruptInstallationNotificationID, + "Install Corruption", + "Portmaster has detected that one or more of its own files have been corrupted. Please re-install the software.", + ) } - u.updateCheckWorkerMgr.Go() - + u.updateCheckWorkerMgr.Delay(15 * time.Second) return nil } -func (u *Updates) GetRootPath() string { - return u.registry.dir +func (u *Updater) GetMainDir() string { + return u.cfg.Directory } // GetFile returns the path of a file given the name. Returns ErrNotFound if file is not found. -func (u *Updates) GetFile(id string) (*File, error) { - file, ok := u.registry.files[id] - if ok { - return &file, nil - } else { - log.Errorf("updates: requested file id not found: %s", id) - return nil, ErrNotFound +func (u *Updater) GetFile(name string) (string, error) { + u.indexLock.Lock() + defer u.indexLock.Unlock() + + for _, artifact := range u.index.Artifacts { + switch { + case artifact.Filename != name: + // Name does not match. + case artifact.Platform != "" && artifact.Platform != currentPlatform: + // Platform is defined and does not match. + // Platforms are usually pre-filtered, but just to be sure. + default: + // Artifact matches! + return filepath.Join(u.cfg.Directory, artifact.Filename), nil + } } + + return "", ErrNotFound } // Stop stops the module. -func (u *Updates) Stop() error { +func (u *Updater) Stop() error { return nil } diff --git a/service/updates/registry.go b/service/updates/registry.go deleted file mode 100644 index 98df62a0..00000000 --- a/service/updates/registry.go +++ /dev/null @@ -1,249 +0,0 @@ -package updates - -import ( - "fmt" - "io" - "os" - "path/filepath" - "strings" - - semver "github.com/hashicorp/go-version" - - "github.com/safing/portmaster/base/log" -) - -const ( - defaultFileMode = os.FileMode(0o0644) - executableFileMode = os.FileMode(0o0744) - executableUIFileMode = os.FileMode(0o0755) - defaultDirMode = os.FileMode(0o0755) -) - -type Registry struct { - bundle *Bundle - dir string - purgeDir string - files map[string]File - - version *semver.Version -} - -func CreateRegistry(index UpdateIndex) (Registry, error) { - registry := Registry{ - dir: index.Directory, - purgeDir: index.PurgeDirectory, - files: make(map[string]File), - } - // Parse bundle - var err error - registry.bundle, err = LoadBundle(filepath.Join(index.Directory, index.IndexFile)) - if err != nil { - return Registry{}, err - } - - // Parse version - registry.version, err = semver.NewVersion(registry.bundle.Version) - if err != nil { - log.Errorf("updates: failed to parse current version: %s", err) - } - - // Process files - for _, artifact := range registry.bundle.Artifacts { - artifactPath := filepath.Join(registry.dir, artifact.Filename) - registry.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath, version: registry.bundle.Version, sha256: artifact.SHA256} - } - return registry, nil -} - -func CreateRegistryFromFolder(index UpdateIndex) (Registry, error) { - registry := Registry{ - dir: index.Directory, - purgeDir: index.PurgeDirectory, - files: make(map[string]File), - } - - files, err := os.ReadDir(index.Directory) - if err != nil { - return Registry{}, nil - } - for _, file := range files { - // Skip dirs - if file.IsDir() { - continue - } - - // Skip the uninstaller. (Windows) - if strings.Contains(strings.ToLower(file.Name()), "uninstall") { - continue - } - - artifactPath := filepath.Join(registry.dir, file.Name()) - registry.files[file.Name()] = File{id: file.Name(), path: artifactPath, version: "", sha256: ""} - } - - return registry, nil -} - -func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { - // Make sure provided update is valid - indexFilepath := filepath.Join(downloadDir, indexFile) - bundle, err := LoadBundle(indexFilepath) - if err != nil { - return fmt.Errorf("invalid update: %w", err) - } - - err = bundle.Verify(downloadDir) - if err != nil { - return fmt.Errorf("invalid update: %w", err) - } - - // Make sure purge dir is empty. - _ = os.RemoveAll(r.purgeDir) - - // Create purge dir. - err = os.MkdirAll(r.purgeDir, defaultDirMode) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - - // Move current version files into purge folder. - log.Debugf("updates: removing the old version") - for _, file := range r.files { - purgePath := filepath.Join(r.purgeDir, file.id) - err := moveFile(file.path, purgePath) - if err != nil { - return fmt.Errorf("failed to move file %s: %w", file.path, err) - } - } - - // Move the new index file - log.Debugf("updates: installing the new version") - newIndexFile := filepath.Join(r.dir, indexFile) - err = moveFile(indexFilepath, newIndexFile) - if err != nil { - return fmt.Errorf("failed to move index file %s: %w", indexFile, err) - } - - // Move downloaded files to the current version folder. - for _, artifact := range bundle.Artifacts { - fromFilepath := filepath.Join(downloadDir, artifact.Filename) - toFilepath := filepath.Join(r.dir, artifact.Filename) - err = moveFile(fromFilepath, toFilepath) - if err != nil { - return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) - } else { - log.Debugf("updates: %s moved", artifact.Filename) - } - } - - log.Infof("updates: update complete") - - return nil -} - -func moveFile(currentPath, newPath string) error { - err := os.Rename(currentPath, newPath) - if err == nil { - // Moving was successful return - return nil - } - - log.Debugf("updates: failed to move '%s' fallback to copy+delete: %s -> %s", err, currentPath, newPath) - - // Failed to move, try copy and delete - currentFile, err := os.Open(currentPath) - if err != nil { - return err - } - defer func() { _ = currentFile.Close() }() - - newFile, err := os.Create(newPath) - if err != nil { - return err - } - defer func() { _ = newFile.Close() }() - - _, err = io.Copy(newFile, currentFile) - if err != nil { - return err - } - - // Make sure file is closed before deletion. - _ = currentFile.Close() - currentFile = nil - - err = os.Remove(currentPath) - if err != nil { - log.Errorf("updates: failed to delete while moving file: %s", err) - } - - return nil -} - -func (r *Registry) performRecoverableUpgrade(downloadDir string, indexFile string) error { - upgradeError := r.performUpgrade(downloadDir, indexFile) - if upgradeError != nil { - err := r.recover() - recoverStatus := "(recovery successful)" - if err != nil { - recoverStatus = "(recovery failed)" - log.Errorf("updates: failed to recover: %s", err) - } - - return fmt.Errorf("upgrade failed: %w %s", upgradeError, recoverStatus) - } - return nil -} - -func (r *Registry) recover() error { - files, err := os.ReadDir(r.purgeDir) - if err != nil { - return err - } - - for _, file := range files { - recoverPath := filepath.Join(r.purgeDir, file.Name()) - currentFilepath := filepath.Join(r.dir, file.Name()) - err := moveFile(recoverPath, currentFilepath) - if err != nil { - return err - } - } - - return nil -} - -func (r *Registry) CleanOldFiles() error { - err := os.RemoveAll(r.purgeDir) - if err != nil { - return fmt.Errorf("failed to delete folder: %w", err) - } - return nil -} - -type File struct { - id string - path string - version string - sha256 string -} - -// Identifier return the id of the file witch is the same as the filename. -func (f *File) Identifier() string { - return f.id -} - -// Path returns the path + filename of the file. -func (f *File) Path() string { - return f.path -} - -// Version returns the version of the file. (currently not filled). -func (f *File) Version() string { - return f.version -} - -// Sha256 returns the sha356 sum of the file. -func (f *File) Sha256() string { - return f.sha256 -} diff --git a/service/updates/signing.go b/service/updates/signing.go new file mode 100644 index 00000000..71e66a5f --- /dev/null +++ b/service/updates/signing.go @@ -0,0 +1,29 @@ +package updates + +import "github.com/safing/jess" + +var ( + // BinarySigningKeys holds the signing keys in text format. + BinarySigningKeys = []string{ + // Safing Code Signing Key #1 + "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", + // Safing Code Signing Key #2 + "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", + } + + // BinarySigningTrustStore is an in-memory trust store with the signing keys. + BinarySigningTrustStore = jess.NewMemTrustStore() +) + +func init() { + for _, signingKey := range BinarySigningKeys { + rcpt, err := jess.RecipientFromTextFormat(signingKey) + if err != nil { + panic(err) + } + err = BinarySigningTrustStore.StoreSignet(rcpt) + if err != nil { + panic(err) + } + } +} diff --git a/service/updates/updates_test.go b/service/updates/updates_test.go index d2857aa5..7ca86445 100644 --- a/service/updates/updates_test.go +++ b/service/updates/updates_test.go @@ -56,7 +56,7 @@ func TestPreformUpdate(t *testing.T) { } // Create updater - updates, err := New(stub, "Test", UpdateIndex{ + updates, err := New(stub, "Test", Config{ Directory: installedDir, DownloadDirectory: updateDir, PurgeDirectory: purgeDir, diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go new file mode 100644 index 00000000..795dfb4a --- /dev/null +++ b/service/updates/upgrade.go @@ -0,0 +1,196 @@ +package updates + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/safing/portmaster/base/log" +) + +const ( + defaultFileMode = os.FileMode(0o0644) + executableFileMode = os.FileMode(0o0744) + executableUIFileMode = os.FileMode(0o0755) + defaultDirMode = os.FileMode(0o0755) +) + +func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error { + // Lock index for the upgrade. + u.indexLock.Lock() + defer u.indexLock.Unlock() + + // Check if we should upgrade at all. + if !ignoreVersion { + if err := u.index.ShouldUpgradeTo(downloader.index); err != nil { + return fmt.Errorf("cannot upgrade: %w", ErrNoUpdateAvailable) + } + } + + // Execute the upgrade. + upgradeError := u.upgradeMoveFiles(downloader, ignoreVersion) + if upgradeError == nil { + return nil + } + + // Attempt to recover from failed upgrade. + recoveryErr := u.recoverFromFailedUpgrade() + if recoveryErr == nil { + return fmt.Errorf("upgrade failed, but recovery was successful: %w", upgradeError) + } + + // Recovery failed too. + return fmt.Errorf("upgrade (including recovery) failed: %s", upgradeError) +} + +func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) error { + // Important: + // We assume that the downloader has done its job and all artifacts are verified. + // Files will just be moved here. + // In case the files are copied, they are verified in the process. + + // Reset purge directory, so that we can do a clean rollback later. + _ = os.RemoveAll(u.cfg.PurgeDirectory) + err := os.MkdirAll(u.cfg.PurgeDirectory, defaultDirMode) + if err != nil { + return fmt.Errorf("failed to create purge directory: %w", err) + } + + // Move current version files into purge folder. + log.Debugf("updates: removing the old version (v%s from %s)", u.index.Version, u.index.Published) + files, err := os.ReadDir(u.cfg.Directory) + if err != nil { + return fmt.Errorf("read current directory: %w", err) + } + for _, file := range files { + // Check if file is ignored. + if slices.Contains(u.cfg.Ignore, file.Name()) { + continue + } + + // Otherwise, move file to purge dir. + src := filepath.Join(u.cfg.Directory, file.Name()) + dst := filepath.Join(u.cfg.PurgeDirectory, file.Name()) + err := moveFile(src, dst, "", file.Type().Perm()) + if err != nil { + return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err) + } + } + + // Move the new index file into main directory. + log.Debugf("updates: installing the new version (v%s from %s)", u.index.Version, u.index.Published) + src := filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile) + dst := filepath.Join(u.cfg.Directory, u.cfg.IndexFile) + err = moveFile(src, dst, "", defaultFileMode) + if err != nil { + return fmt.Errorf("failed to move index file to %s: %w", dst, err) + } + + // Move downloaded files to the current version folder. + for _, artifact := range downloader.index.Artifacts { + src = filepath.Join(u.cfg.DownloadDirectory, artifact.Filename) + dst = filepath.Join(u.cfg.Directory, artifact.Filename) + err = moveFile(src, dst, artifact.SHA256, artifact.GetFileMode()) + if err != nil { + return fmt.Errorf("failed to move file %s: %w", artifact.Filename, err) + } else { + log.Debugf("updates: %s moved", artifact.Filename) + } + } + + // Set new index on module. + u.index = downloader.index + log.Infof("updates: update complete (v%s from %s)", u.index.Version, u.index.Published) + + return nil +} + +// moveFile moves a file and falls back to copying if it fails. +func moveFile(currentPath, newPath string, sha256sum string, fileMode fs.FileMode) error { + // Try to simply move file. + err := os.Rename(currentPath, newPath) + if err == nil { + // Moving was successful, return. + return nil + } + log.Tracef("updates: failed to move to %q, falling back to copy+delete: %w", newPath, err) + + // Copy and check the checksum while we are at it. + err = copyAndCheckSHA256Sum(currentPath, newPath, sha256sum, fileMode) + if err != nil { + return fmt.Errorf("move failed, copy+delete fallback failed: %w", err) + } + + return nil +} + +// recoverFromFailedUpgrade attempts to roll back any moved files by the upgrade process. +func (u *Updater) recoverFromFailedUpgrade() error { + // Get list of files from purge dir. + files, err := os.ReadDir(u.cfg.PurgeDirectory) + if err != nil { + return err + } + + // Move all files back to main dir. + for _, file := range files { + purgedFile := filepath.Join(u.cfg.PurgeDirectory, file.Name()) + activeFile := filepath.Join(u.cfg.Directory, file.Name()) + err := moveFile(purgedFile, activeFile, "", file.Type().Perm()) + if err != nil { + // Only warn and continue to recover as many files as possible. + log.Warningf("updates: failed to roll back file %s: %w", file.Name(), err) + } + } + + return nil +} + +func (u *Updater) cleanupAfterUpgrade() error { + err := os.RemoveAll(u.cfg.PurgeDirectory) + if err != nil { + return fmt.Errorf("delete purge dir: %w", err) + } + + err = os.RemoveAll(u.cfg.DownloadDirectory) + if err != nil { + return fmt.Errorf("delete download dir: %w", err) + } + + return nil +} + +func (u *Updater) deleteUnfinishedFiles(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + return err + } + + for _, e := range entries { + switch { + case e.IsDir(): + // Continue. + + case strings.HasSuffix(e.Name(), ".download"): + path := filepath.Join(dir, e.Name()) + log.Warningf("updates: deleting unfinished download file: %s\n", path) + err := os.Remove(path) + if err != nil { + log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err) + } + + case strings.HasSuffix(e.Name(), ".copy"): + path := filepath.Join(dir, e.Name()) + log.Warningf("updates: deleting unfinished copied file: %s\n", path) + err := os.Remove(path) + if err != nil { + log.Errorf("updates: failed to delete unfinished copied file %s: %s", path, err) + } + } + } + + return nil +} diff --git a/spn/captain/module.go b/spn/captain/module.go index 2cd16719..c5279c4d 100644 --- a/spn/captain/module.go +++ b/spn/captain/module.go @@ -249,6 +249,6 @@ type instance interface { NetEnv() *netenv.NetEnv Patrol() *patrol.Patrol Config() *config.Config - IntelUpdates() *updates.Updates + IntelUpdates() *updates.Updater SPNGroup() *mgr.ExtendedGroup } diff --git a/spn/hub/hub_test.go b/spn/hub/hub_test.go index d82bd6cc..09425e72 100644 --- a/spn/hub/hub_test.go +++ b/spn/hub/hub_test.go @@ -20,11 +20,11 @@ type testInstance struct { db *dbmodule.DBModule api *api.API config *config.Config - updates *updates.Updates + updates *updates.Updater base *base.Base } -func (stub *testInstance) IntelUpdates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updater { return stub.updates } @@ -86,7 +86,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + stub.updates, err = updates.New(stub, "Test Intel", updates.Config{ Directory: installDir, IndexFile: "index.json", }) diff --git a/spn/instance.go b/spn/instance.go index ed942933..5c9b3957 100644 --- a/spn/instance.go +++ b/spn/instance.go @@ -49,8 +49,8 @@ type Instance struct { rng *rng.Rng core *core.Core - binaryUpdates *updates.Updates - intelUpdates *updates.Updates + binaryUpdates *updates.Updater + intelUpdates *updates.Updater geoip *geoip.GeoIP netenv *netenv.NetEnv filterLists *filterlists.FilterLists @@ -75,11 +75,11 @@ func New() (*Instance, error) { instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) - binaryUpdateIndex := updates.UpdateIndex{ + binaryUpdateIndex := updates.Config{ // FIXME: fill } - intelUpdateIndex := updates.UpdateIndex{ + intelUpdateIndex := updates.Config{ // FIXME: fill } @@ -270,12 +270,12 @@ func (i *Instance) Base() *base.Base { } // BinaryUpdates returns the updates module. -func (i *Instance) BinaryUpdates() *updates.Updates { +func (i *Instance) BinaryUpdates() *updates.Updater { return i.binaryUpdates } // IntelUpdates returns the updates module. -func (i *Instance) IntelUpdates() *updates.Updates { +func (i *Instance) IntelUpdates() *updates.Updater { return i.intelUpdates } diff --git a/spn/navigator/module_test.go b/spn/navigator/module_test.go index 6ad2ea46..a35cf9f6 100644 --- a/spn/navigator/module_test.go +++ b/spn/navigator/module_test.go @@ -19,12 +19,12 @@ type testInstance struct { db *dbmodule.DBModule api *api.API config *config.Config - updates *updates.Updates + updates *updates.Updater base *base.Base geoip *geoip.GeoIP } -func (stub *testInstance) IntelUpdates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updater { return stub.updates } @@ -88,7 +88,7 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + stub.updates, err = updates.New(stub, "Test Intel", updates.Config{ Directory: installDir, IndexFile: "index.json", }) From 7bc1c3b76488eb060201c2df9c3401065784dc24 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 6 Nov 2024 10:48:02 +0100 Subject: [PATCH 55/62] Replace dataroot module with BinDir and DataDir on instance, adapt modules --- base/config/init_test.go | 50 +++++--- base/config/main.go | 39 +----- base/config/module.go | 1 + base/database/database_test.go | 2 +- base/database/dbmodule/db.go | 27 +++-- base/database/main.go | 40 +++---- base/dataroot/root.go | 25 ---- base/metrics/metrics_host.go | 12 +- base/metrics/module.go | 4 +- cmds/portmaster-core/main.go | 111 +++++------------- cmds/portmaster-core/run.go | 108 +++++++++++++++++ .../{main_linux.go => run_linux.go} | 2 +- .../{main_windows.go => run_windows.go} | 0 cmds/portmaster-core/update.go | 77 ++++++++++++ cmds/updatemgr/scan.go | 8 +- service/broadcasts/install_info.go | 4 + service/config.go | 88 +++++++++++++- service/core/base/global.go | 26 ---- service/core/base/logs.go | 3 +- service/core/base/module.go | 1 + service/firewall/api.go | 9 +- service/firewall/module.go | 1 + service/instance.go | 107 ++++++----------- service/intel/filterlists/database.go | 8 +- service/intel/filterlists/index.go | 2 +- service/intel/filterlists/updater.go | 96 ++++++++------- service/intel/geoip/database.go | 83 +++++++------ service/intel/geoip/module.go | 1 + service/netquery/database.go | 14 +-- service/netquery/module_api.go | 1 + service/profile/module.go | 10 +- service/profile/profile.go | 2 +- service/ui/module.go | 7 +- service/updates.go | 106 +++++++++++++++++ service/updates/downloader.go | 8 +- service/updates/index.go | 92 ++++++++++++++- service/updates/module.go | 58 ++++++--- service/updates/upgrade.go | 34 +++--- spn/captain/intel.go | 34 ++++-- 39 files changed, 819 insertions(+), 482 deletions(-) delete mode 100644 base/dataroot/root.go create mode 100644 cmds/portmaster-core/run.go rename cmds/portmaster-core/{main_linux.go => run_linux.go} (97%) rename cmds/portmaster-core/{main_windows.go => run_windows.go} (100%) create mode 100644 cmds/portmaster-core/update.go create mode 100644 service/updates.go diff --git a/base/config/init_test.go b/base/config/init_test.go index 53967044..e3390379 100644 --- a/base/config/init_test.go +++ b/base/config/init_test.go @@ -6,30 +6,50 @@ import ( "testing" ) -type testInstance struct{} +type testInstance struct { + dataDir string +} var _ instance = testInstance{} +func (stub testInstance) DataDir() string { + return stub.dataDir +} + func (stub testInstance) SetCmdLineOperation(f func() error) {} -func runTest(m *testing.M) error { - ds, err := InitializeUnitTestDataroot("test-config") +func newTestInstance(testName string) (*testInstance, error) { + testDir, err := os.MkdirTemp("", fmt.Sprintf("portmaster-%s", testName)) if err != nil { - return fmt.Errorf("failed to initialize dataroot: %w", err) - } - defer func() { _ = os.RemoveAll(ds) }() - module, err = New(&testInstance{}) - if err != nil { - return fmt.Errorf("failed to initialize module: %w", err) + return nil, fmt.Errorf("failed to make tmp dir: %w", err) } - m.Run() - return nil + return &testInstance{ + dataDir: testDir, + }, nil } -func TestMain(m *testing.M) { - if err := runTest(m); err != nil { - fmt.Printf("%s\n", err) - os.Exit(1) +func TestConfigPersistence(t *testing.T) { + t.Parallel() + + instance, err := newTestInstance("test-config") + if err != nil { + t.Fatalf("failed to create test instance: %s", err) + } + defer func() { _ = os.RemoveAll(instance.DataDir()) }() + + module, err = New(instance) + if err != nil { + t.Fatalf("failed to initialize module: %s", err) + } + + err = SaveConfig() + if err != nil { + t.Fatal(err) + } + + err = loadConfig(true) + if err != nil { + t.Fatal(err) } } diff --git a/base/config/main.go b/base/config/main.go index 0ed0b7e6..dc10544c 100644 --- a/base/config/main.go +++ b/base/config/main.go @@ -10,8 +10,6 @@ import ( "path/filepath" "sort" - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/base/utils/debug" "github.com/safing/portmaster/service/mgr" ) @@ -19,29 +17,13 @@ import ( // ChangeEvent is the name of the config change event. const ChangeEvent = "config change" -var ( - dataRoot *utils.DirStructure - - exportConfig bool -) - -// SetDataRoot sets the data root from which the updates module derives its paths. -func SetDataRoot(root *utils.DirStructure) { - if dataRoot == nil { - dataRoot = root - } -} +var exportConfig bool func init() { flag.BoolVar(&exportConfig, "export-config-options", false, "export configuration registry and exit") } func prep() error { - SetDataRoot(dataroot.Root()) - if dataRoot == nil { - return errors.New("data root is not set") - } - if exportConfig { module.instance.SetCmdLineOperation(exportConfigCmd) return mgr.ErrExecuteCmdLineOp @@ -51,7 +33,7 @@ func prep() error { } func start() error { - configFilePath = filepath.Join(dataRoot.Path, "config.json") + configFilePath = filepath.Join(module.instance.DataDir(), "config.json") // Load log level from log package after it started. err := loadLogLevel() @@ -136,20 +118,3 @@ func GetActiveConfigValues() map[string]interface{} { return values } - -// InitializeUnitTestDataroot initializes a new random tmp directory for running tests. -func InitializeUnitTestDataroot(testName string) (string, error) { - basePath, err := os.MkdirTemp("", fmt.Sprintf("portmaster-%s", testName)) - if err != nil { - return "", fmt.Errorf("failed to make tmp dir: %w", err) - } - - ds := utils.NewDirStructure(basePath, 0o0755) - SetDataRoot(ds) - err = dataroot.Initialize(basePath, 0o0755) - if err != nil { - return "", fmt.Errorf("failed to initialize dataroot: %w", err) - } - - return basePath, nil -} diff --git a/base/config/module.go b/base/config/module.go index 465e5454..d44abe1b 100644 --- a/base/config/module.go +++ b/base/config/module.go @@ -56,5 +56,6 @@ func New(instance instance) (*Config, error) { } type instance interface { + DataDir() string SetCmdLineOperation(f func() error) } diff --git a/base/database/database_test.go b/base/database/database_test.go index 03dcc66a..be4971dd 100644 --- a/base/database/database_test.go +++ b/base/database/database_test.go @@ -26,7 +26,7 @@ func TestMain(m *testing.M) { panic(err) } - err = InitializeWithPath(testDir) + err = Initialize(testDir) if err != nil { panic(err) } diff --git a/base/database/dbmodule/db.go b/base/database/dbmodule/db.go index 99991c88..51d121fd 100644 --- a/base/database/dbmodule/db.go +++ b/base/database/dbmodule/db.go @@ -2,11 +2,10 @@ package dbmodule import ( "errors" + "path/filepath" "sync/atomic" "github.com/safing/portmaster/base/database" - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/service/mgr" ) @@ -27,18 +26,18 @@ func (dbm *DBModule) Stop() error { return stop() } -var databaseStructureRoot *utils.DirStructure +var databasesRootDir string // SetDatabaseLocation sets the location of the database for initialization. Supply either a path or dir structure. -func SetDatabaseLocation(dirStructureRoot *utils.DirStructure) { - if databaseStructureRoot == nil { - databaseStructureRoot = dirStructureRoot +func SetDatabaseLocation(dir string) { + if databasesRootDir == "" { + databasesRootDir = dir } } func prep() error { - SetDatabaseLocation(dataroot.Root()) - if databaseStructureRoot == nil { + SetDatabaseLocation(filepath.Join(module.instance.DataDir(), "databases")) + if databasesRootDir == "" { return errors.New("database location not specified") } @@ -64,16 +63,16 @@ func New(instance instance) (*DBModule, error) { return nil, errors.New("only one instance allowed") } - if err := prep(); err != nil { - return nil, err - } m := mgr.New("DBModule") module = &DBModule{ mgr: m, instance: instance, } + if err := prep(); err != nil { + return nil, err + } - err := database.Initialize(databaseStructureRoot) + err := database.Initialize(databasesRootDir) if err != nil { return nil, err } @@ -81,4 +80,6 @@ func New(instance instance) (*DBModule, error) { return module, nil } -type instance interface{} +type instance interface { + DataDir() string +} diff --git a/base/database/main.go b/base/database/main.go index f84a0108..ed0bb934 100644 --- a/base/database/main.go +++ b/base/database/main.go @@ -3,14 +3,10 @@ package database import ( "errors" "fmt" + "os" + "path/filepath" "github.com/tevino/abool" - - "github.com/safing/portmaster/base/utils" -) - -const ( - databasesSubDir = "databases" ) var ( @@ -19,25 +15,18 @@ var ( shuttingDown = abool.NewBool(false) shutdownSignal = make(chan struct{}) - rootStructure *utils.DirStructure - databasesStructure *utils.DirStructure + rootDir string ) -// InitializeWithPath initializes the database at the specified location using a path. -func InitializeWithPath(dirPath string) error { - return Initialize(utils.NewDirStructure(dirPath, 0o0755)) -} - -// Initialize initializes the database at the specified location using a dir structure. -func Initialize(dirStructureRoot *utils.DirStructure) error { +// Initialize initializes the database at the specified location. +func Initialize(databasesRootDir string) error { if initialized.SetToIf(false, true) { - rootStructure = dirStructureRoot + rootDir = databasesRootDir - // ensure root and databases dirs - databasesStructure = rootStructure.ChildDir(databasesSubDir, 0o0700) - err := databasesStructure.Ensure() + // Ensure database root dir exists. + err := os.MkdirAll(rootDir, 0o0700) if err != nil { - return fmt.Errorf("could not create/open database directory (%s): %w", rootStructure.Path, err) + return fmt.Errorf("could not create/open database directory (%s): %w", rootDir, err) } return nil @@ -67,11 +56,12 @@ func Shutdown() (err error) { // getLocation returns the storage location for the given name and type. func getLocation(name, storageType string) (string, error) { - location := databasesStructure.ChildDir(name, 0o0700).ChildDir(storageType, 0o0700) - // check location - err := location.Ensure() + location := filepath.Join(rootDir, name, storageType) + + // Make sure location exists. + err := os.MkdirAll(location, 0o0700) if err != nil { - return "", fmt.Errorf(`failed to create/check database dir "%s": %w`, location.Path, err) + return "", fmt.Errorf("failed to create/check database dir %q: %w", location, err) } - return location.Path, nil + return location, nil } diff --git a/base/dataroot/root.go b/base/dataroot/root.go deleted file mode 100644 index 296b342f..00000000 --- a/base/dataroot/root.go +++ /dev/null @@ -1,25 +0,0 @@ -package dataroot - -import ( - "errors" - "os" - - "github.com/safing/portmaster/base/utils" -) - -var root *utils.DirStructure - -// Initialize initializes the data root directory. -func Initialize(rootDir string, perm os.FileMode) error { - if root != nil { - return errors.New("already initialized") - } - - root = utils.NewDirStructure(rootDir, perm) - return root.Ensure() -} - -// Root returns the data root directory. -func Root() *utils.DirStructure { - return root -} diff --git a/base/metrics/metrics_host.go b/base/metrics/metrics_host.go index 5b632a8d..2dd557f6 100644 --- a/base/metrics/metrics_host.go +++ b/base/metrics/metrics_host.go @@ -10,7 +10,6 @@ import ( "github.com/shirou/gopsutil/mem" "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" ) @@ -209,18 +208,9 @@ func getDiskStat() *disk.UsageStat { return diskStat } - // Check if we have a data root. - dataRoot := dataroot.Root() - if dataRoot == nil { - log.Warning("metrics: cannot get disk stats without data root") - diskStat = nil - diskStatExpires = time.Now().Add(hostStatTTL) - return diskStat - } - // Refresh. var err error - diskStat, err = disk.Usage(dataRoot.Path) + diskStat, err = disk.Usage(module.instance.DataDir()) if err != nil { log.Warningf("metrics: failed to get load avg: %s", err) diskStat = nil diff --git a/base/metrics/module.go b/base/metrics/module.go index a1e5bd37..48a1c2b7 100644 --- a/base/metrics/module.go +++ b/base/metrics/module.go @@ -213,4 +213,6 @@ func New(instance instance) (*Metrics, error) { return module, nil } -type instance interface{} +type instance interface { + DataDir() string +} diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 7a5dc5a2..eb677a47 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -1,47 +1,49 @@ package main import ( - "bufio" - "errors" "flag" "fmt" - "io" - "log/slog" "os" "runtime" - "runtime/pprof" - "syscall" + + "github.com/spf13/cobra" "github.com/safing/portmaster/base/info" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service" - "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/spn/conf" ) var ( - printStackOnExit bool - enableInputSignals bool + rootCmd = &cobra.Command{ + Use: "portmaster-core", + PersistentPreRun: initializeGlobals, + Run: cmdRun, + } - sigUSR1 = syscall.Signal(0xa) // dummy for windows + binDir string + dataDir string + + svcCfg *service.ServiceConfig ) func init() { - flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") - flag.BoolVar(&enableInputSignals, "input-signals", false, "emulate signals using stdin") + // Add Go's default flag set. + rootCmd.Flags().AddGoFlagSet(flag.CommandLine) + + // Add persisent flags for all commands. + rootCmd.PersistentFlags().StringVar(&binDir, "bin-dir", "", "set directory for executable binaries (rw/ro)") + rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "set directory for variable data (rw)") } func main() { - flag.Parse() - // Call platform specific checks, that will execute commands like "recover-iptables" - platformSpecificChecks() - - instance := initialize() - run(instance) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } } -func initialize() *service.Instance { +func initializeGlobals(cmd *cobra.Command, args []string) { // set information info.Set("Portmaster", "", "GPLv3") @@ -51,66 +53,13 @@ func initialize() *service.Instance { // Configure user agent. updates.UserAgent = fmt.Sprintf("Portmaster Core (%s %s)", runtime.GOOS, runtime.GOARCH) - // enable SPN client mode - conf.EnableClient(true) - conf.EnableIntegration(true) - - // Create instance. - var execCmdLine bool - instance, err := service.New(&service.ServiceConfig{}) - switch { - case err == nil: - // Continue - case errors.Is(err, mgr.ErrExecuteCmdLineOp): - execCmdLine = true - default: - fmt.Printf("error creating an instance: %s\n", err) - os.Exit(2) - } - - // Execute command line operation, if requested or available. - switch { - case !execCmdLine: - // Run service. - case instance.CommandLineOperation == nil: - fmt.Println("command line operation execution requested, but not set") - os.Exit(3) - default: - // Run the function and exit. - err = instance.CommandLineOperation() - if err != nil { - fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err) - os.Exit(3) - } - os.Exit(0) - } - return instance -} - -func printStackTo(writer io.Writer, msg string) { - _, err := fmt.Fprintf(writer, "===== %s =====\n", msg) - if err == nil { - err = pprof.Lookup("goroutine").WriteTo(writer, 1) - } - if err != nil { - slog.Error("failed to write stack trace", "err", err) - } -} - -func inputSignals(signalCh chan os.Signal) { - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - switch scanner.Text() { - case "SIGHUP": - signalCh <- syscall.SIGHUP - case "SIGINT": - signalCh <- syscall.SIGINT - case "SIGQUIT": - signalCh <- syscall.SIGQUIT - case "SIGTERM": - signalCh <- syscall.SIGTERM - case "SIGUSR1": - signalCh <- sigUSR1 - } + // Create service config. + svcCfg = &service.ServiceConfig{ + BinDir: binDir, + DataDir: dataDir, + BinariesIndexURLs: service.DefaultBinaryIndexURLs, + IntelIndexURLs: service.DefaultIntelIndexURLs, + VerifyBinaryUpdates: service.BinarySigningTrustStore, + VerifyIntelUpdates: service.BinarySigningTrustStore, } } diff --git a/cmds/portmaster-core/run.go b/cmds/portmaster-core/run.go new file mode 100644 index 00000000..0cbe4812 --- /dev/null +++ b/cmds/portmaster-core/run.go @@ -0,0 +1,108 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "io" + "log/slog" + "os" + "runtime/pprof" + "syscall" + + "github.com/spf13/cobra" + + "github.com/safing/portmaster/service" + "github.com/safing/portmaster/service/mgr" + "github.com/safing/portmaster/spn/conf" +) + +var ( + printStackOnExit bool + enableInputSignals bool + + sigUSR1 = syscall.Signal(0xa) // dummy for windows +) + +func init() { + flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") + flag.BoolVar(&enableInputSignals, "input-signals", false, "emulate signals using stdin") +} + +func cmdRun(cmd *cobra.Command, args []string) { + // Call platform specific checks, that will execute commands like "recover-iptables" + platformSpecificChecks() + + svcCfg.VerifyBinaryUpdates = nil // FIXME + svcCfg.VerifyIntelUpdates = nil // FIXME + + instance := createInstance() + run(instance) +} + +func createInstance() *service.Instance { + // enable SPN client mode + conf.EnableClient(true) + conf.EnableIntegration(true) + + // Create instance. + var execCmdLine bool + instance, err := service.New(svcCfg) + switch { + case err == nil: + // Continue + case errors.Is(err, mgr.ErrExecuteCmdLineOp): + execCmdLine = true + default: + fmt.Printf("error creating an instance: %s\n", err) + os.Exit(2) + } + + // Execute module command line operation, if requested or available. + switch { + case !execCmdLine: + // Run service. + case instance.CommandLineOperation == nil: + fmt.Println("command line operation execution requested, but not set") + os.Exit(3) + default: + // Run the function and exit. + fmt.Println("executing cmdline op") + err = instance.CommandLineOperation() + if err != nil { + fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err) + os.Exit(3) + } + os.Exit(0) + } + return instance +} + +func printStackTo(writer io.Writer, msg string) { + _, err := fmt.Fprintf(writer, "===== %s =====\n", msg) + if err == nil { + err = pprof.Lookup("goroutine").WriteTo(writer, 1) + } + if err != nil { + slog.Error("failed to write stack trace", "err", err) + } +} + +func inputSignals(signalCh chan os.Signal) { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + switch scanner.Text() { + case "SIGHUP": + signalCh <- syscall.SIGHUP + case "SIGINT": + signalCh <- syscall.SIGINT + case "SIGQUIT": + signalCh <- syscall.SIGQUIT + case "SIGTERM": + signalCh <- syscall.SIGTERM + case "SIGUSR1": + signalCh <- sigUSR1 + } + } +} diff --git a/cmds/portmaster-core/main_linux.go b/cmds/portmaster-core/run_linux.go similarity index 97% rename from cmds/portmaster-core/main_linux.go rename to cmds/portmaster-core/run_linux.go index 026cd249..25c6da0b 100644 --- a/cmds/portmaster-core/main_linux.go +++ b/cmds/portmaster-core/run_linux.go @@ -129,7 +129,7 @@ func isRunningAsService() bool { // Get the current process ID pid := os.Getpid() - currentProcess, err := processInfo.NewProcess(int32(pid)) + currentProcess, err := processInfo.NewProcess(int32(pid)) //nolint:gosec if err != nil { return false } diff --git a/cmds/portmaster-core/main_windows.go b/cmds/portmaster-core/run_windows.go similarity index 100% rename from cmds/portmaster-core/main_windows.go rename to cmds/portmaster-core/run_windows.go diff --git a/cmds/portmaster-core/update.go b/cmds/portmaster-core/update.go new file mode 100644 index 00000000..ceb0ff6c --- /dev/null +++ b/cmds/portmaster-core/update.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/base/notifications" + "github.com/safing/portmaster/service" + "github.com/safing/portmaster/service/updates" +) + +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Force an update of all components.", + RunE: update, +} + +func init() { + rootCmd.AddCommand(updateCmd) +} + +func update(cmd *cobra.Command, args []string) error { + // Finalize config. + svcCfg.VerifyBinaryUpdates = nil // FIXME + svcCfg.VerifyIntelUpdates = nil // FIXME + err := svcCfg.Init() + if err != nil { + return fmt.Errorf("internal configuration error: %w", err) + } + + // Start logging. + log.SetLogLevel(log.InfoLevel) + _ = log.Start() + defer log.Shutdown() + + // Create updaters. + instance := &updateDummyInstance{} + binaryUpdateConfig, intelUpdateConfig, err := service.MakeUpdateConfigs(svcCfg) + if err != nil { + return fmt.Errorf("init updater config: %w", err) + } + binaryUpdates, err := updates.New(instance, "Binary Updater", *binaryUpdateConfig) + if err != nil { + return fmt.Errorf("configure binary updates: %w", err) + } + intelUpdates, err := updates.New(instance, "Intel Updater", *intelUpdateConfig) + if err != nil { + return fmt.Errorf("configure intel updates: %w", err) + } + + // Force update all. + binErr := binaryUpdates.ForceUpdate() + if binErr != nil { + log.Errorf("binary update failed: %s", binErr) + } + intelErr := intelUpdates.ForceUpdate() + if intelErr != nil { + log.Errorf("intel update failed: %s", intelErr) + } + + // Return error. + if binErr != nil { + return fmt.Errorf("binary update failed: %w", binErr) + } + if intelErr != nil { + return fmt.Errorf("intel update failed: %w", intelErr) + } + return nil +} + +type updateDummyInstance struct{} + +func (udi *updateDummyInstance) Restart() {} +func (udi *updateDummyInstance) Shutdown() {} +func (udi *updateDummyInstance) Notifications() *notifications.Notifications { return nil } diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go index 06435bf4..db60058f 100644 --- a/cmds/updatemgr/scan.go +++ b/cmds/updatemgr/scan.go @@ -9,7 +9,7 @@ import ( ) var ( - bundleSettings = updates.IndexScanConfig{ + scanConfig = updates.IndexScanConfig{ Name: "Portmaster Binaries", PrimaryArtifact: "linux_amd64/portmaster-core", BaseURL: "https://updates.safing.io/", @@ -60,17 +60,17 @@ var ( RunE: scan, } - bundleDir string + scanDir string ) func init() { rootCmd.AddCommand(scanCmd) - scanCmd.Flags().StringVarP(&bundleDir, "dir", "d", "", "directory to create index from (required)") + scanCmd.Flags().StringVarP(&scanDir, "dir", "d", "", "directory to create index from (required)") _ = scanCmd.MarkFlagRequired("dir") } func scan(cmd *cobra.Command, args []string) error { - bundle, err := updates.GenerateBundleFromDir(bundleDir, bundleSettings) + bundle, err := updates.GenerateIndexFromDir(scanDir, scanConfig) if err != nil { return err } diff --git a/service/broadcasts/install_info.go b/service/broadcasts/install_info.go index 42969946..7f95de53 100644 --- a/service/broadcasts/install_info.go +++ b/service/broadcasts/install_info.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "sync" "time" @@ -111,6 +112,9 @@ func (ii *InstallInfo) checkVersion() { // MakeNumericVersion makes a numeric version with the first three version // segment always using three digits. func MakeNumericVersion(version string) (numericVersion int64, err error) { + // Remove any comments. + version = strings.SplitN(version, " ", 2)[0] + // Parse version string. ver, err := semver.NewVersion(version) if err != nil { diff --git a/service/config.go b/service/config.go index 85a98603..b9d9b5c1 100644 --- a/service/config.go +++ b/service/config.go @@ -1,3 +1,89 @@ package service -type ServiceConfig struct{} +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/safing/jess" +) + +type ServiceConfig struct { + BinDir string + DataDir string + + BinariesIndexURLs []string + IntelIndexURLs []string + VerifyBinaryUpdates jess.TrustStore + VerifyIntelUpdates jess.TrustStore +} + +func (sc *ServiceConfig) Init() error { + // Check directories + switch runtime.GOOS { + case "windows": + if sc.BinDir == "" { + exeDir, err := getCurrentBinaryFolder() // Default: C:/Program Files/Portmaster + if err != nil { + return fmt.Errorf("derive bin dir from runnning exe: %w", err) + } + sc.BinDir = exeDir + } + if sc.DataDir == "" { + sc.DataDir = filepath.FromSlash("$ProgramData/Portmaster") + } + + case "linux": + // Fall back to defaults. + if sc.BinDir == "" { + sc.BinDir = "/usr/lib/portmaster" + } + if sc.DataDir == "" { + sc.DataDir = "/var/lib/portmaster" + } + + default: + // Fail if not configured on other platforms. + if sc.BinDir == "" { + return errors.New("binary directory must be configured - auto-detection not supported on this platform") + } + if sc.DataDir == "" { + return errors.New("binary directory must be configured - auto-detection not supported on this platform") + } + } + + // Expand path variables. + sc.BinDir = os.ExpandEnv(sc.BinDir) + sc.DataDir = os.ExpandEnv(sc.DataDir) + + // Apply defaults for required fields. + if len(sc.BinariesIndexURLs) == 0 { + sc.BinariesIndexURLs = DefaultBinaryIndexURLs + } + if len(sc.IntelIndexURLs) == 0 { + sc.IntelIndexURLs = DefaultIntelIndexURLs + } + + return nil +} + +func getCurrentBinaryFolder() (string, error) { + // Get the path of the currently running executable + exePath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("failed to get executable path: %w", err) + } + + // Get the absolute path + absPath, err := filepath.Abs(exePath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + // Get the directory of the executable + installDir := filepath.Dir(absPath) + + return installDir, nil +} diff --git a/service/core/base/global.go b/service/core/base/global.go index 3b1cc82f..912e975a 100644 --- a/service/core/base/global.go +++ b/service/core/base/global.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/info" "github.com/safing/portmaster/service/mgr" ) @@ -15,14 +14,10 @@ import ( var ( DefaultAPIListenAddress = "127.0.0.1:817" - dataDir string - databaseDir string showVersion bool ) func init() { - flag.StringVar(&dataDir, "data", "", "set data directory") - flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)") flag.BoolVar(&showVersion, "version", false, "show version and exit") } @@ -39,27 +34,6 @@ func prep(instance instance) error { return mgr.ErrExecuteCmdLineOp } - // check data root - if dataroot.Root() == nil { - // initialize data dir - - // backwards compatibility - if dataDir == "" { - dataDir = databaseDir - } - - // check data dir - if dataDir == "" { - return errors.New("please set the data directory using --data=/path/to/data/dir") - } - - // initialize structure - err := dataroot.Initialize(dataDir, 0o0755) - if err != nil { - return err - } - } - // set api listen address api.SetDefaultAPIListenAddress(DefaultAPIListenAddress) diff --git a/service/core/base/logs.go b/service/core/base/logs.go index 8ceab9cd..921d7f3a 100644 --- a/service/core/base/logs.go +++ b/service/core/base/logs.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" ) @@ -26,7 +25,7 @@ func logCleaner(_ *mgr.WorkerCtx) error { ageThreshold := time.Now().Add(-logTTL) return filepath.Walk( - filepath.Join(dataroot.Root().Path, logFileDir), + filepath.Join(module.instance.DataDir(), logFileDir), func(path string, info os.FileInfo, err error) error { if err != nil { if !errors.Is(err, os.ErrNotExist) { diff --git a/service/core/base/module.go b/service/core/base/module.go index 66492015..0bb7aba2 100644 --- a/service/core/base/module.go +++ b/service/core/base/module.go @@ -58,5 +58,6 @@ func New(instance instance) (*Base, error) { } type instance interface { + DataDir() string SetCmdLineOperation(f func() error) } diff --git a/service/firewall/api.go b/service/firewall/api.go index 50bafa39..07153263 100644 --- a/service/firewall/api.go +++ b/service/firewall/api.go @@ -11,9 +11,7 @@ import ( "time" "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/service/netenv" "github.com/safing/portmaster/service/network/netutils" "github.com/safing/portmaster/service/network/packet" @@ -38,15 +36,12 @@ For production use please create an API key in the settings.` ) var ( - dataRoot *utils.DirStructure - apiPortSet bool apiIP net.IP apiPort uint16 ) func prepAPIAuth() error { - dataRoot = dataroot.Root() return api.SetAuthenticator(apiAuthenticator) } @@ -132,7 +127,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo var originalPid int // Get authenticated path. - authenticatedPath := module.instance.BinaryUpdates().GetRootPath() + authenticatedPath := module.instance.BinaryUpdates().GetMainDir() if authenticatedPath == "" { return false, fmt.Errorf(deniedMsgMisconfigured, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user } @@ -214,7 +209,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo return false, fmt.Errorf(deniedMsgSystem, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user default: // normal process - log.Tracer(ctx).Warningf("filter: denying api access to %s - also checked %s (trusted root is %s)", procsChecked[0], strings.Join(procsChecked[1:], " "), dataRoot.Path) + log.Tracer(ctx).Warningf("filter: denying api access to %s - also checked %s (trusted root is %s)", procsChecked[0], strings.Join(procsChecked[1:], " "), module.instance.BinDir()) return false, fmt.Errorf( //nolint:stylecheck // message for user deniedMsgUnauthorized, api.ErrAPIAccessDeniedMessage, diff --git a/service/firewall/module.go b/service/firewall/module.go index 056f59bb..70226e55 100644 --- a/service/firewall/module.go +++ b/service/firewall/module.go @@ -160,6 +160,7 @@ func New(instance instance) (*Firewall, error) { } type instance interface { + BinDir() string Config() *config.Config BinaryUpdates() *updates.Updater Profile() *profile.ProfileModule diff --git a/service/instance.go b/service/instance.go index cb050a99..5bfeb7a1 100644 --- a/service/instance.go +++ b/service/instance.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" - go_runtime "runtime" "sync/atomic" "time" @@ -55,6 +53,9 @@ type Instance struct { cancelCtx context.CancelFunc serviceGroup *mgr.Group + binDir string + dataDir string + exitCode atomic.Int32 database *dbmodule.DBModule @@ -105,83 +106,27 @@ type Instance struct { ShouldRestart bool } -func getCurrentBinaryFolder() (string, error) { - // Get the path of the currently running executable - exePath, err := os.Executable() - if err != nil { - return "", fmt.Errorf("failed to get executable path: %w", err) - } - - // Get the absolute path - absPath, err := filepath.Abs(exePath) - if err != nil { - return "", fmt.Errorf("failed to get absolute path: %w", err) - } - - // Get the directory of the executable - installDir := filepath.Dir(absPath) - - return installDir, nil -} - // New returns a new Portmaster service instance. func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx - var binaryUpdateIndex updates.Config - var intelUpdateIndex updates.Config - if go_runtime.GOOS == "windows" { - binaryFolder, err := getCurrentBinaryFolder() - if err != nil { - return nil, err - } - binaryUpdateIndex = updates.Config{ - Directory: binaryFolder, // Default: C:/Program Files/Portmaster - DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_binary"), - PurgeDirectory: filepath.Join(binaryFolder, "old_binary"), // Default: C:/Program Files/Portmaster/old_binary - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, - NeedsRestart: true, - } + // Initialize config. + err := svcCfg.Init() + if err != nil { + return nil, fmt.Errorf("internal service config error: %w", err) + } - intelUpdateIndex = updates.Config{ - Directory: os.ExpandEnv("$ProgramData/Portmaster/intel"), - DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_intel"), - PurgeDirectory: os.ExpandEnv("$ProgramData/Portmaster/old_intel"), - IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, - NeedsRestart: false, - } - } else if go_runtime.GOOS == "linux" { - binaryUpdateIndex = updates.Config{ - Directory: "/usr/lib/portmaster", - DownloadDirectory: "/var/lib/portmaster/new_bin", - PurgeDirectory: "/var/lib/portmaster/old_bin", - Ignore: []string{"databases", "intel", "config.json"}, - IndexURLs: []string{"http://localhost:8000/test-binary.json"}, - IndexFile: "bin-index.json", - AutoApply: false, - NeedsRestart: true, - } - - intelUpdateIndex = updates.Config{ - Directory: "/var/lib/portmaster/intel", - DownloadDirectory: "/var/lib/portmaster/new_intel", - PurgeDirectory: "/var/lib/portmaster/intel_bin", - IndexURLs: []string{"http://localhost:8000/test-intel.json"}, - IndexFile: "intel-index.json", - AutoApply: true, - NeedsRestart: false, - } + // Make sure data dir exists, so that child directories don't dictate the permissions. + err = os.MkdirAll(svcCfg.DataDir, 0o0755) + if err != nil { + return nil, fmt.Errorf("data directory %s is not accessible: %w", svcCfg.DataDir, err) } // Create instance to pass it to modules. - instance := &Instance{} + instance := &Instance{ + binDir: svcCfg.BinDir, + dataDir: svcCfg.DataDir, + } instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) - var err error - // Base modules instance.base, err = base.New(instance) if err != nil { @@ -221,11 +166,15 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx if err != nil { return instance, fmt.Errorf("create core module: %w", err) } - instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex) + binaryUpdateConfig, intelUpdateConfig, err := MakeUpdateConfigs(svcCfg) + if err != nil { + return instance, fmt.Errorf("create updates config: %w", err) + } + instance.binaryUpdates, err = updates.New(instance, "Binary Updater", *binaryUpdateConfig) if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } - instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex) + instance.intelUpdates, err = updates.New(instance, "Intel Updater", *intelUpdateConfig) if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } @@ -413,6 +362,18 @@ func (i *Instance) SetSleep(enabled bool) { } } +// BinDir returns the directory for binaries. +// This directory may be read-only. +func (i *Instance) BinDir() string { + return i.binDir +} + +// DataDir returns the directory for variable data. +// This directory is expected to be read/writeable. +func (i *Instance) DataDir() string { + return i.dataDir +} + // Database returns the database module. func (i *Instance) Database() *dbmodule.DBModule { return i.database diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go index 84f6e813..4d5c5c93 100644 --- a/service/intel/filterlists/database.go +++ b/service/intel/filterlists/database.go @@ -39,9 +39,9 @@ var ( filterListLock sync.RWMutex // Updater files for tracking upgrades. - baseFile *updates.File - intermediateFile *updates.File - urgentFile *updates.File + baseFile *updates.Artifact + intermediateFile *updates.Artifact + urgentFile *updates.Artifact filterListsLoaded chan struct{} ) @@ -77,7 +77,7 @@ func isLoaded() bool { // processListFile opens the latest version of file and decodes it's DSDL // content. It calls processEntry for each decoded filterlists entry. -func processListFile(ctx context.Context, filter *scopedBloom, file *updates.File) error { +func processListFile(ctx context.Context, filter *scopedBloom, file *updates.Artifact) error { f, err := os.Open(file.Path()) if err != nil { return err diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go index daebef52..9ba0ec17 100644 --- a/service/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -162,7 +162,7 @@ func getListIndexFromCache() (*ListIndexFile, error) { var ( // listIndexUpdate must only be used by updateListIndex. - listIndexUpdate *updates.File + listIndexUpdate *updates.Artifact listIndexUpdateLock sync.Mutex ) diff --git a/service/intel/filterlists/updater.go b/service/intel/filterlists/updater.go index e36198e1..cd7e9200 100644 --- a/service/intel/filterlists/updater.go +++ b/service/intel/filterlists/updater.go @@ -63,7 +63,7 @@ func performUpdate(ctx context.Context) error { // First, update the list index. err := updateListIndex() if err != nil { - log.Errorf("intel/filterlists: failed update list index: %s", err) + log.Warningf("intel/filterlists: failed update list index: %s", err) } upgradables, err := getUpgradableFiles() @@ -83,7 +83,7 @@ func performUpdate(ctx context.Context) error { // perform the actual upgrade by processing each file // in the returned order. for idx, file := range upgradables { - log.Debugf("intel/filterlists: applying update (%d) %s version %s", idx, file.Identifier(), file.Version()) + log.Debugf("intel/filterlists: applying update (%d) %s version %s", idx, file.Filename, file.Version) if file == baseFile { if idx != 0 { @@ -101,7 +101,7 @@ func performUpdate(ctx context.Context) error { } if err := processListFile(ctx, filterToUpdate, file); err != nil { - return fmt.Errorf("failed to process upgrade %s: %w", file.Identifier(), err) + return fmt.Errorf("failed to process upgrade %s version %s: %w", file.Filename, file.Version, err) } } @@ -145,10 +145,10 @@ func performUpdate(ctx context.Context) error { // try to save the highest version of our files. highestVersion := upgradables[len(upgradables)-1] - if err := setCacheDatabaseVersion(highestVersion.Version()); err != nil { + if err := setCacheDatabaseVersion(highestVersion.Version); err != nil { log.Errorf("intel/filterlists: failed to save cache database version: %s", err) } else { - log.Infof("intel/filterlists: successfully migrated cache database to %s", highestVersion.Version()) + log.Infof("intel/filterlists: successfully migrated cache database to %s", highestVersion.Version) } // The list update succeeded, resolve any states. @@ -174,51 +174,51 @@ func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error { // getUpgradableFiles returns a slice of filterlists files // that should be updated. The files MUST be updated and // processed in the returned order! -func getUpgradableFiles() ([]*updates.File, error) { - var updateOrder []*updates.File +func getUpgradableFiles() ([]*updates.Artifact, error) { + var updateOrder []*updates.Artifact - // cacheDBInUse := isLoaded() + cacheDBInUse := isLoaded() - // if baseFile == nil || !cacheDBInUse { // TODO(vladimir): || baseFile.UpgradeAvailable() - // var err error - // baseFile, err = module.instance.Updates().GetFile(baseListFilePath) - // if err != nil { - // return nil, err - // } - // log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version()) - // updateOrder = append(updateOrder, baseFile) - // } + newBaseFile, err := module.instance.IntelUpdates().GetFile(baseListFilePath) + if err != nil { + log.Warningf("intel/filterlists: failed to get base update: %s", err) + } else if newer, _ := newBaseFile.IsNewerThan(baseFile); newer || !cacheDBInUse { + log.Tracef("intel/filterlists: base file needs update to version %s", newBaseFile.Version) + if newBaseFile.SemVer() == nil { + log.Warningf("intel/filterlists: base file needs update to version %s, but semver is invalid", newBaseFile.Version) + } else { + updateOrder = append(updateOrder, newBaseFile) + } + } - // if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse { - // var err error - // intermediateFile, err = getFile(intermediateListFilePath) - // if err != nil && !errors.Is(err, updater.ErrNotFound) { - // return nil, err - // } + newIntermediateFile, err := module.instance.IntelUpdates().GetFile(intermediateListFilePath) + if err != nil { + log.Warningf("intel/filterlists: failed to get intermediate update: %s", err) + } else if newer, _ := newIntermediateFile.IsNewerThan(intermediateFile); newer || !cacheDBInUse { + log.Tracef("intel/filterlists: intermediate file needs update to version %s", newIntermediateFile.Version) + if newIntermediateFile.SemVer() == nil { + log.Warningf("intel/filterlists: intermediate file needs update to version %s, but semver is invalid", newIntermediateFile.Version) + } else { + updateOrder = append(updateOrder, newIntermediateFile) + } + } - // if err == nil { - // log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version()) - // updateOrder = append(updateOrder, intermediateFile) - // } - // } - - // if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse { - // var err error - // urgentFile, err = getFile(urgentListFilePath) - // if err != nil && !errors.Is(err, updater.ErrNotFound) { - // return nil, err - // } - - // if err == nil { - // log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version()) - // updateOrder = append(updateOrder, urgentFile) - // } - // } + newUrgentFile, err := module.instance.IntelUpdates().GetFile(urgentListFilePath) + if err != nil { + log.Warningf("intel/filterlists: failed to get urgent update: %s", err) + } else if newer, _ := newUrgentFile.IsNewerThan(urgentFile); newer || !cacheDBInUse { + log.Tracef("intel/filterlists: urgent file needs update to version %s", newUrgentFile.Version) + if newUrgentFile.SemVer() == nil { + log.Warningf("intel/filterlists: urgent file needs update to version %s, but semver is invalid", newUrgentFile.Version) + } else { + updateOrder = append(updateOrder, newUrgentFile) + } + } return resolveUpdateOrder(updateOrder) } -func resolveUpdateOrder(updateOrder []*updates.File) ([]*updates.File, error) { +func resolveUpdateOrder(updateOrder []*updates.Artifact) ([]*updates.Artifact, error) { // sort the update order by ascending version sort.Sort(byAscVersion(updateOrder)) log.Tracef("intel/filterlists: order of updates: %v", updateOrder) @@ -239,9 +239,8 @@ func resolveUpdateOrder(updateOrder []*updates.File) ([]*updates.File, error) { startAtIdx := -1 for idx, file := range updateOrder { - ver, _ := version.NewSemver(file.Version()) - log.Tracef("intel/filterlists: checking file with version %s against %s", ver, cacheDBVersion) - if ver.GreaterThan(cacheDBVersion) && (startAtIdx == -1 || file == baseFile) { + log.Tracef("intel/filterlists: checking file with version %s against %s", file.SemVer(), cacheDBVersion) + if file.SemVer().GreaterThan(cacheDBVersion) && (startAtIdx == -1 || file == baseFile) { startAtIdx = idx } } @@ -258,15 +257,12 @@ func resolveUpdateOrder(updateOrder []*updates.File) ([]*updates.File, error) { return updateOrder[startAtIdx:], nil } -type byAscVersion []*updates.File +type byAscVersion []*updates.Artifact func (fs byAscVersion) Len() int { return len(fs) } func (fs byAscVersion) Less(i, j int) bool { - vi, _ := version.NewSemver(fs[i].Version()) - vj, _ := version.NewSemver(fs[j].Version()) - - return vi.LessThan(vj) + return fs[i].SemVer().LessThan(fs[j].SemVer()) } func (fs byAscVersion) Swap(i, j int) { diff --git a/service/intel/geoip/database.go b/service/intel/geoip/database.go index a860bec1..711fafdf 100644 --- a/service/intel/geoip/database.go +++ b/service/intel/geoip/database.go @@ -17,6 +17,12 @@ var worker *updateWorker func init() { worker = &updateWorker{ trigger: make(chan struct{}), + v4: updateBroadcaster{ + dbName: v4MMDBResource, + }, + v6: updateBroadcaster{ + dbName: v6MMDBResource, + }, } } @@ -27,26 +33,50 @@ const ( type geoIPDB struct { *maxminddb.Reader - file *updates.File + update *updates.Artifact } // updateBroadcaster stores a geoIPDB and provides synchronized // access to the MMDB reader. It also supports broadcasting to // multiple waiters when a new database becomes available. type updateBroadcaster struct { - rw sync.RWMutex - db *geoIPDB + rw sync.RWMutex + db *geoIPDB + dbName string waiter chan struct{} } -// NeedsUpdate returns true if the current broadcaster needs a -// database update. -func (ub *updateBroadcaster) NeedsUpdate() bool { +// AvailableUpdate returns a new update artifact if the current broadcaster +// needs a database update. +func (ub *updateBroadcaster) AvailableUpdate() *updates.Artifact { ub.rw.RLock() defer ub.rw.RUnlock() - return ub.db == nil // TODO(vladimir) is this needed: || ub.db.file.UpgradeAvailable() + // Get artifact. + artifact, err := module.instance.IntelUpdates().GetFile(ub.dbName) + if err != nil { + // Check if the geoip database is included in the binary index instead. + // TODO: Remove when intelhub builds the geoip database. + if artifact2, err2 := module.instance.BinaryUpdates().GetFile(ub.dbName); err2 == nil { + artifact = artifact2 + err = nil + } else { + log.Warningf("geoip: failed to get geoip update: %s", err) + return nil + } + } + + // Return artifact if not yet initialized. + if ub.db == nil { + return artifact + } + + // Compare and return artifact only when confirmed newer. + if newer, _ := artifact.IsNewerThan(ub.db.update); newer { + return artifact + } + return nil } // ReplaceDatabase replaces (or initially sets) the mmdb database. @@ -153,16 +183,18 @@ func (upd *updateWorker) start() { func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error { for { - if upd.v4.NeedsUpdate() { - if v4, err := getGeoIPDB(v4MMDBResource); err == nil { + update := upd.v4.AvailableUpdate() + if update != nil { + if v4, err := getGeoIPDB(update); err == nil { upd.v4.ReplaceDatabase(v4) } else { log.Warningf("geoip: failed to get v4 database: %s", err) } } - if upd.v6.NeedsUpdate() { - if v6, err := getGeoIPDB(v6MMDBResource); err == nil { + update = upd.v6.AvailableUpdate() + if update != nil { + if v6, err := getGeoIPDB(update); err == nil { upd.v6.ReplaceDatabase(v6) } else { log.Warningf("geoip: failed to get v6 database: %s", err) @@ -177,36 +209,17 @@ func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error { } } -func getGeoIPDB(resource string) (*geoIPDB, error) { - log.Debugf("geoip: opening database %s", resource) +func getGeoIPDB(update *updates.Artifact) (*geoIPDB, error) { + log.Debugf("geoip: opening database %s", update.Path()) - file, err := open(resource) - if err != nil { - return nil, err - } - - reader, err := maxminddb.Open(file.Path()) + reader, err := maxminddb.Open(update.Path()) if err != nil { return nil, fmt.Errorf("failed to open: %w", err) } - log.Debugf("geoip: successfully opened database %s", resource) + log.Debugf("geoip: successfully opened database %s", update.Filename) return &geoIPDB{ Reader: reader, - file: file, + update: update, }, nil } - -func open(resource string) (*updates.File, error) { - f, err := module.instance.IntelUpdates().GetFile(resource) - if err != nil { - return nil, fmt.Errorf("getting file: %w", err) - } - - // unpacked, err := f.Unpack(".gz", updater.UnpackGZIP) - // if err != nil { - // return nil, "", fmt.Errorf("unpacking file: %w", err) - // } - - return f, nil -} diff --git a/service/intel/geoip/module.go b/service/intel/geoip/module.go index 01c3797a..770d018b 100644 --- a/service/intel/geoip/module.go +++ b/service/intel/geoip/module.go @@ -66,5 +66,6 @@ func New(instance instance) (*GeoIP, error) { } type instance interface { + BinaryUpdates() *updates.Updater IntelUpdates() *updates.Updater } diff --git a/service/netquery/database.go b/service/netquery/database.go index a1cd6aea..d66e3222 100644 --- a/service/netquery/database.go +++ b/service/netquery/database.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "os" "path/filepath" "sort" "strings" @@ -17,7 +18,6 @@ import ( "zombiezen.com/go/sqlite/sqlitex" "github.com/safing/portmaster/base/config" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/netquery/orm" "github.com/safing/portmaster/service/network" @@ -127,13 +127,13 @@ type ( // Note that write connections are serialized by the Database object before being // handed over to SQLite. func New(dbPath string) (*Database, error) { - historyParentDir := dataroot.Root().ChildDir("databases", 0o700) - if err := historyParentDir.Ensure(); err != nil { + historyParentDir := filepath.Join(module.instance.DataDir(), "databases") + if err := os.MkdirAll(historyParentDir, 0o0700); err != nil { return nil, fmt.Errorf("failed to ensure database directory exists: %w", err) } // Get file location of history database. - historyFile := filepath.Join(historyParentDir.Path, "history.db") + historyFile := filepath.Join(historyParentDir, "history.db") // Convert to SQLite URI path. historyURI := "file:///" + strings.TrimPrefix(filepath.ToSlash(historyFile), "/") @@ -225,13 +225,13 @@ func (db *Database) Close() error { // VacuumHistory rewrites the history database in order to purge deleted records. func VacuumHistory(ctx context.Context) (err error) { - historyParentDir := dataroot.Root().ChildDir("databases", 0o700) - if err := historyParentDir.Ensure(); err != nil { + historyParentDir := filepath.Join(module.instance.DataDir(), "databases") + if err := os.MkdirAll(historyParentDir, 0o0700); err != nil { return fmt.Errorf("failed to ensure database directory exists: %w", err) } // Get file location of history database. - historyFile := filepath.Join(historyParentDir.Path, "history.db") + historyFile := filepath.Join(historyParentDir, "history.db") // Convert to SQLite URI path. historyURI := "file:///" + strings.TrimPrefix(filepath.ToSlash(historyFile), "/") diff --git a/service/netquery/module_api.go b/service/netquery/module_api.go index 497226b0..5beb9044 100644 --- a/service/netquery/module_api.go +++ b/service/netquery/module_api.go @@ -310,5 +310,6 @@ func NewModule(instance instance) (*NetQuery, error) { } type instance interface { + DataDir() string Profile() *profile.ProfileModule } diff --git a/service/profile/module.go b/service/profile/module.go index 67873b8c..68845047 100644 --- a/service/profile/module.go +++ b/service/profile/module.go @@ -3,12 +3,13 @@ package profile import ( "errors" "fmt" + "os" + "path/filepath" "sync/atomic" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database/migration" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" _ "github.com/safing/portmaster/service/core/base" "github.com/safing/portmaster/service/mgr" @@ -65,11 +66,11 @@ func prep() error { } // Setup icon storage location. - iconsDir := dataroot.Root().ChildDir("databases", 0o0700).ChildDir("icons", 0o0700) - if err := iconsDir.Ensure(); err != nil { + iconsDir := filepath.Join(module.instance.DataDir(), "databases", "icons") + if err := os.MkdirAll(iconsDir, 0o0700); err != nil { return fmt.Errorf("failed to create/check icons directory: %w", err) } - binmeta.ProfileIconStoragePath = iconsDir.Path + binmeta.ProfileIconStoragePath = iconsDir return nil } @@ -151,5 +152,6 @@ func NewModule(instance instance) (*ProfileModule, error) { } type instance interface { + DataDir() string Config() *config.Config } diff --git a/service/profile/profile.go b/service/profile/profile.go index 97bdca8c..2db0e3a9 100644 --- a/service/profile/profile.go +++ b/service/profile/profile.go @@ -197,7 +197,7 @@ func (profile *Profile) parseConfig() error { if ok { profile.filterListIDs, err = filterlists.ResolveListIDs(list) if err != nil { - lastErr = err + log.Warningf("profiles: failed to resolve filter list IDs: %s", err) } else { profile.filterListsSet = true } diff --git a/service/ui/module.go b/service/ui/module.go index 805d0c09..36fa5ac6 100644 --- a/service/ui/module.go +++ b/service/ui/module.go @@ -2,10 +2,11 @@ package ui import ( "errors" + "os" + "path/filepath" "sync/atomic" "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/updates" @@ -28,7 +29,8 @@ func start() error { // may seem dangerous, but proper permission on the parent directory provide // (some) protection. // Processes must _never_ read from this directory. - err := dataroot.Root().ChildDir("exec", 0o0777).Ensure() + execDir := filepath.Join(module.instance.DataDir(), "exec") + err := os.MkdirAll(execDir, 0o0777) //nolint:gosec // This is intentional. if err != nil { log.Warningf("ui: failed to create safe exec dir: %s", err) } @@ -81,6 +83,7 @@ func New(instance instance) (*UI, error) { } type instance interface { + DataDir() string API() *api.API BinaryUpdates() *updates.Updater } diff --git a/service/updates.go b/service/updates.go new file mode 100644 index 00000000..c9ee185e --- /dev/null +++ b/service/updates.go @@ -0,0 +1,106 @@ +package service + +import ( + "path/filepath" + go_runtime "runtime" + + "github.com/safing/jess" + "github.com/safing/portmaster/service/updates" +) + +var ( + DefaultBinaryIndexURLs = []string{ + "https://updates.safing.io/stable.v3.json", + } + DefaultIntelIndexURLs = []string{ + "https://updates.safing.io/intel.v3.json", + } + + // BinarySigningKeys holds the signing keys in text format. + BinarySigningKeys = []string{ + // Safing Code Signing Key #1 + "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", + // Safing Code Signing Key #2 + "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", + } + + // BinarySigningTrustStore is an in-memory trust store with the signing keys. + BinarySigningTrustStore = jess.NewMemTrustStore() +) + +func init() { + for _, signingKey := range BinarySigningKeys { + rcpt, err := jess.RecipientFromTextFormat(signingKey) + if err != nil { + panic(err) + } + err = BinarySigningTrustStore.StoreSignet(rcpt) + if err != nil { + panic(err) + } + } +} + +func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateConfig *updates.Config, err error) { + switch go_runtime.GOOS { + case "windows": + binaryUpdateConfig = &updates.Config{ + Name: "binaries", + Directory: svcCfg.BinDir, + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), + PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"), + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: svcCfg.BinariesIndexURLs, + IndexFile: "index.json", + Verify: svcCfg.VerifyBinaryUpdates, + AutoDownload: false, + AutoApply: false, + NeedsRestart: true, + Notify: true, + } + intelUpdateConfig = &updates.Config{ + Name: "intel", + Directory: filepath.Join(svcCfg.DataDir, "intel"), + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), + PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), + IndexURLs: svcCfg.IntelIndexURLs, + IndexFile: "index.json", + Verify: svcCfg.VerifyIntelUpdates, + AutoDownload: true, + AutoApply: true, + NeedsRestart: false, + Notify: false, + } + + case "linux": + binaryUpdateConfig = &updates.Config{ + Name: "binaries", + Directory: svcCfg.BinDir, + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), + PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"), + Ignore: []string{"databases", "intel", "config.json"}, + IndexURLs: svcCfg.BinariesIndexURLs, + IndexFile: "index.json", + Verify: svcCfg.VerifyBinaryUpdates, + AutoDownload: false, + AutoApply: false, + NeedsRestart: true, + Notify: true, + } + intelUpdateConfig = &updates.Config{ + Name: "intel", + Directory: filepath.Join(svcCfg.DataDir, "intel"), + DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), + PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), + IndexURLs: svcCfg.IntelIndexURLs, + IndexFile: "index.json", + Verify: svcCfg.VerifyIntelUpdates, + AutoDownload: true, + AutoApply: true, + NeedsRestart: false, + Notify: false, + } + } + + return +} diff --git a/service/updates/downloader.go b/service/updates/downloader.go index a8621271..8d0d8b24 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -55,7 +55,7 @@ func (d *Downloader) updateIndex(ctx context.Context) error { break } - log.Warningf("updates: failed to update index from %q: %s", url, err) + log.Warningf("updates/%s: failed to update index from %q: %s", d.u.cfg.Name, url, err) err = fmt.Errorf("update index file from %q: %s", url, err) } if err != nil { @@ -111,7 +111,7 @@ func (d *Downloader) gatherExistingFiles(dir string) error { // Read full file. fileData, err := os.ReadFile(fullpath) if err != nil { - log.Debugf("updates: failed to read file %q while searching for existing files: %w", fullpath, err) + log.Debugf("updates/%s: failed to read file %q while searching for existing files: %w", d.u.cfg.Name, fullpath, err) return fmt.Errorf("failed to read file %s: %w", fullpath, err) } @@ -150,7 +150,7 @@ artifacts: if err == nil { continue artifacts } - log.Debugf("updates: failed to copy existing file %s: %w", artifact.Filename, err) + log.Debugf("updates/%s: failed to copy existing file %s: %w", d.u.cfg.Name, artifact.Filename, err) } // Try to download the artifact from one of the URLs. @@ -182,7 +182,7 @@ artifacts: return fmt.Errorf("rename %s after write: %w", artifact.Filename, err) } - log.Infof("updates: downloaded and verified %s", artifact.Filename) + log.Infof("updates/%s: downloaded and verified %s", d.u.cfg.Name, artifact.Filename) } return nil } diff --git a/service/updates/index.go b/service/updates/index.go index 77b58845..f627d0d3 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -35,7 +35,8 @@ type Artifact struct { Unpack string `json:"Unpack,omitempty"` Version string `json:"Version,omitempty"` - localFile string + localFile string + versionNum *semver.Version } // GetFileMode returns the required filesystem permission for the artifact. @@ -52,6 +53,67 @@ func (a *Artifact) GetFileMode() os.FileMode { return defaultFileMode } +// Path returns the absolute path to the local file. +func (a *Artifact) Path() string { + return a.localFile +} + +// SemVer returns the version of the artifact. +func (a *Artifact) SemVer() *semver.Version { + return a.versionNum +} + +// IsNewerThan returns whether the artifact is newer than the given artifact. +// Returns true if the given artifact is nil. +// The second return value "ok" is false when version could not be compared. +// In this case, it is up to the caller to decide how to proceed. +func (a *Artifact) IsNewerThan(b *Artifact) (newer, ok bool) { + switch { + case a == nil: + return false, false + case b == nil: + return true, true + case a.versionNum == nil: + return false, false + case b.versionNum == nil: + return false, false + case a.versionNum.GreaterThan(b.versionNum): + return true, true + default: + return false, true + } +} + +func (a *Artifact) export(dir string, indexVersion *semver.Version) *Artifact { + copy := &Artifact{ + Filename: a.Filename, + SHA256: a.SHA256, + URLs: a.URLs, + Platform: a.Platform, + Unpack: a.Unpack, + Version: a.Version, + localFile: filepath.Join(dir, a.Filename), + versionNum: a.versionNum, + } + + // Make sure we have a version number. + switch { + case copy.versionNum != nil: + // Version already parsed. + case copy.Version != "": + // Need to parse version. + v, err := semver.NewVersion(copy.Version) + if err == nil { + copy.versionNum = v + } + default: + // No version defined, inherit index version. + copy.versionNum = indexVersion + } + + return copy +} + // Index represents a collection of artifacts with metadata. type Index struct { Name string `json:"Name"` @@ -90,16 +152,26 @@ func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error) return nil, fmt.Errorf("parse index: %w", err) } + // Initialize data. + err = index.init() + if err != nil { + return nil, err + } + + return &index, nil +} + +func (index *Index) init() error { // Parse version number, if set. if index.Version != "" { versionNum, err := semver.NewVersion(index.Version) if err != nil { - return nil, fmt.Errorf("invalid index version %q: %w", index.Version, err) + return fmt.Errorf("invalid index version %q: %w", index.Version, err) } index.versionNum = versionNum } - // Filter artifacts by currnet platform. + // Filter artifacts by current platform. filtered := make([]Artifact, 0) for _, a := range index.Artifacts { if a.Platform == "" || a.Platform == currentPlatform { @@ -108,7 +180,19 @@ func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error) } index.Artifacts = filtered - return &index, nil + // Parse artifact version numbers. + for _, a := range index.Artifacts { + if a.Version != "" { + v, err := semver.NewVersion(a.Version) + if err == nil { + a.versionNum = v + } + } else { + a.versionNum = index.versionNum + } + } + + return nil } // CanDoUpgrades returns whether the index is able to follow a secure upgrade path. diff --git a/service/updates/module.go b/service/updates/module.go index 3cb183a7..607a5512 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -45,6 +45,8 @@ var ( // Config holds the configuration for the updates module. type Config struct { + // Name of the updater. + Name string // Directory is the main directory where the currently to-be-used artifacts live. Directory string // DownloadDirectory is the directory where new artifacts are downloaded to and prepared for upgrading. @@ -80,6 +82,8 @@ type Config struct { func (cfg *Config) Check() error { // Check if required fields are set. switch { + case cfg.Name == "": + return errors.New("name must be set") case cfg.Directory == "": return errors.New("directory must be set") case cfg.DownloadDirectory == "": @@ -157,19 +161,22 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { // Load index. index, err := LoadIndex(filepath.Join(cfg.Directory, cfg.IndexFile), cfg.Verify) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - log.Errorf("updates: invalid index file, falling back to dir scan: %w", err) - } - - // Fall back to scanning the directory. - index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"}) - if err != nil { - return nil, fmt.Errorf("updates index load and dir scan failed: %w", err) - } + if err == nil { + module.index = index + return module, nil } - module.index = index + // Fall back to scanning the directory. + if !errors.Is(err, os.ErrNotExist) { + log.Errorf("updates/%s: invalid index file, falling back to dir scan: %w", cfg.Name, err) + } + index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"}) + if err == nil && index.init() == nil { + module.index = index + return module, nil + } + + // Fall back to empty index. return module, nil } @@ -207,7 +214,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV u.indexLock.Unlock() // Check with local pointer to index. if err := index.ShouldUpgradeTo(downloader.index); err != nil { - log.Infof("updates: no new or eligible update: %s", err) + log.Infof("updates/%s: no new or eligible update: %s", u.cfg.Name, err) if u.cfg.Notify && u.instance.Notifications() != nil { u.instance.Notifications().NotifyInfo( noNewUpdateNotificationID, @@ -247,12 +254,12 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Download any remaining needed files. // If everything is already found in the download directory, then this is a no-op. - log.Infof("updates: downloading new version: %s %s", downloader.index.Name, downloader.index.Version) + log.Infof("updates/%s: downloading new version: %s %s", u.cfg.Name, downloader.index.Name, downloader.index.Version) err = downloader.downloadArtifacts(w.Ctx()) if err != nil { - log.Errorf("updates: failed to download update: %s", err) + log.Errorf("updates/%s: failed to download update: %s", u.cfg.Name, err) if err := u.deleteUnfinishedFiles(u.cfg.DownloadDirectory); err != nil { - log.Debugf("updates: failed to delete unfinished files in download directory %s", u.cfg.DownloadDirectory) + log.Debugf("updates/%s: failed to delete unfinished files in download directory %s", u.cfg.Name, u.cfg.DownloadDirectory) } return fmt.Errorf("downloading failed: %w", err) } @@ -282,7 +289,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV err = u.upgrade(downloader, ignoreVersion) if err != nil { if err := u.deleteUnfinishedFiles(u.cfg.PurgeDirectory); err != nil { - log.Debugf("updates: failed to delete unfinished files in purge directory %s", u.cfg.PurgeDirectory) + log.Debugf("updates/%s: failed to delete unfinished files in purge directory %s", u.cfg.Name, u.cfg.PurgeDirectory) } return err } @@ -334,6 +341,14 @@ func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error { return nil } +// ForceUpdate executes a forced update and upgrade directly and synchronously +// and is intended to be used only within a tool, not a service. +func (u *Updater) ForceUpdate() error { + return u.m.Do("update and upgrade", func(w *mgr.WorkerCtx) error { + return u.updateAndUpgrade(w, u.cfg.IndexURLs, true, true) + }) +} + // UpdateFromURL installs an update from the provided url. func (u *Updater) UpdateFromURL(url string) error { u.m.Go("custom update from url", func(w *mgr.WorkerCtx) error { @@ -383,10 +398,15 @@ func (u *Updater) GetMainDir() string { } // GetFile returns the path of a file given the name. Returns ErrNotFound if file is not found. -func (u *Updater) GetFile(name string) (string, error) { +func (u *Updater) GetFile(name string) (*Artifact, error) { u.indexLock.Lock() defer u.indexLock.Unlock() + // Check if any index is active. + if u.index == nil { + return nil, ErrNotFound + } + for _, artifact := range u.index.Artifacts { switch { case artifact.Filename != name: @@ -396,11 +416,11 @@ func (u *Updater) GetFile(name string) (string, error) { // Platforms are usually pre-filtered, but just to be sure. default: // Artifact matches! - return filepath.Join(u.cfg.Directory, artifact.Filename), nil + return artifact.export(u.cfg.Directory, u.index.versionNum), nil } } - return "", ErrNotFound + return nil, ErrNotFound } // Stop stops the module. diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go index 795dfb4a..ddb86bfa 100644 --- a/service/updates/upgrade.go +++ b/service/updates/upgrade.go @@ -43,7 +43,7 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error { } // Recovery failed too. - return fmt.Errorf("upgrade (including recovery) failed: %s", upgradeError) + return fmt.Errorf("upgrade (including recovery) failed: %s", u.cfg.Name, upgradeError) } func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) error { @@ -60,7 +60,9 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e } // Move current version files into purge folder. - log.Debugf("updates: removing the old version (v%s from %s)", u.index.Version, u.index.Published) + if u.index != nil { + log.Debugf("updates/%s: removing the old version (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published) + } files, err := os.ReadDir(u.cfg.Directory) if err != nil { return fmt.Errorf("read current directory: %w", err) @@ -74,17 +76,17 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e // Otherwise, move file to purge dir. src := filepath.Join(u.cfg.Directory, file.Name()) dst := filepath.Join(u.cfg.PurgeDirectory, file.Name()) - err := moveFile(src, dst, "", file.Type().Perm()) + err := u.moveFile(src, dst, "", file.Type().Perm()) if err != nil { return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err) } } // Move the new index file into main directory. - log.Debugf("updates: installing the new version (v%s from %s)", u.index.Version, u.index.Published) + log.Debugf("updates/%s: installing the new version (v%s from %s)", u.cfg.Name, downloader.index.Version, downloader.index.Published) src := filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile) dst := filepath.Join(u.cfg.Directory, u.cfg.IndexFile) - err = moveFile(src, dst, "", defaultFileMode) + err = u.moveFile(src, dst, "", defaultFileMode) if err != nil { return fmt.Errorf("failed to move index file to %s: %w", dst, err) } @@ -93,30 +95,30 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e for _, artifact := range downloader.index.Artifacts { src = filepath.Join(u.cfg.DownloadDirectory, artifact.Filename) dst = filepath.Join(u.cfg.Directory, artifact.Filename) - err = moveFile(src, dst, artifact.SHA256, artifact.GetFileMode()) + err = u.moveFile(src, dst, artifact.SHA256, artifact.GetFileMode()) if err != nil { return fmt.Errorf("failed to move file %s: %w", artifact.Filename, err) } else { - log.Debugf("updates: %s moved", artifact.Filename) + log.Debugf("updates/%s: %s moved", u.cfg.Name, artifact.Filename) } } // Set new index on module. u.index = downloader.index - log.Infof("updates: update complete (v%s from %s)", u.index.Version, u.index.Published) + log.Infof("updates/%s: update complete (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published) return nil } // moveFile moves a file and falls back to copying if it fails. -func moveFile(currentPath, newPath string, sha256sum string, fileMode fs.FileMode) error { +func (u *Updater) moveFile(currentPath, newPath string, sha256sum string, fileMode fs.FileMode) error { // Try to simply move file. err := os.Rename(currentPath, newPath) if err == nil { // Moving was successful, return. return nil } - log.Tracef("updates: failed to move to %q, falling back to copy+delete: %w", newPath, err) + log.Tracef("updates/%s: failed to move to %q, falling back to copy+delete: %w", u.cfg.Name, newPath, err) // Copy and check the checksum while we are at it. err = copyAndCheckSHA256Sum(currentPath, newPath, sha256sum, fileMode) @@ -139,10 +141,10 @@ func (u *Updater) recoverFromFailedUpgrade() error { for _, file := range files { purgedFile := filepath.Join(u.cfg.PurgeDirectory, file.Name()) activeFile := filepath.Join(u.cfg.Directory, file.Name()) - err := moveFile(purgedFile, activeFile, "", file.Type().Perm()) + err := u.moveFile(purgedFile, activeFile, "", file.Type().Perm()) if err != nil { // Only warn and continue to recover as many files as possible. - log.Warningf("updates: failed to roll back file %s: %w", file.Name(), err) + log.Warningf("updates/%s: failed to roll back file %s: %w", u.cfg.Name, file.Name(), err) } } @@ -176,18 +178,18 @@ func (u *Updater) deleteUnfinishedFiles(dir string) error { case strings.HasSuffix(e.Name(), ".download"): path := filepath.Join(dir, e.Name()) - log.Warningf("updates: deleting unfinished download file: %s\n", path) + log.Warningf("updates/%s: deleting unfinished download file: %s", u.cfg.Name, path) err := os.Remove(path) if err != nil { - log.Errorf("updates: failed to delete unfinished download file %s: %s", path, err) + log.Errorf("updates/%s: failed to delete unfinished download file %s: %s", u.cfg.Name, path, err) } case strings.HasSuffix(e.Name(), ".copy"): path := filepath.Join(dir, e.Name()) - log.Warningf("updates: deleting unfinished copied file: %s\n", path) + log.Warningf("updates/%s: deleting unfinished copied file: %s", u.cfg.Name, path) err := os.Remove(path) if err != nil { - log.Errorf("updates: failed to delete unfinished copied file %s: %s", path, err) + log.Errorf("updates/%s: failed to delete unfinished copied file %s: %s", u.cfg.Name, path, err) } } } diff --git a/spn/captain/intel.go b/spn/captain/intel.go index ac1145a6..085bf35c 100644 --- a/spn/captain/intel.go +++ b/spn/captain/intel.go @@ -15,8 +15,8 @@ import ( ) var ( - intelResource *updates.File - intelResourcePath = "intel/spn/main-intel.yaml" + intelResource *updates.Artifact + intelResourceName = "main-intel.yaml" intelResourceMapName = "main" intelResourceUpdateLock sync.Mutex ) @@ -42,18 +42,21 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { return fmt.Errorf("intel resource not for map %q", conf.MainMapName) } - // Check if there is something to do. - // TODO(vladimir): is update check needed - if intelResource != nil { // && !intelResource.UpgradeAvailable() { - return nil - } - - // Get intel file and load it from disk. - intelResource, err = module.instance.IntelUpdates().GetFile(intelResourcePath) + // Get possibly updated file. + file, err := module.instance.IntelUpdates().GetFile(intelResourceName) if err != nil { return fmt.Errorf("failed to get SPN intel update: %w", err) } - intelData, err := os.ReadFile(intelResource.Path()) + + // Check if file is newer. + // Continue on check failure. + newer, ok := file.IsNewerThan(intelResource) + if ok && !newer { + return nil + } + + // Load intel file from disk. + intelData, err := os.ReadFile(file.Path()) if err != nil { return fmt.Errorf("failed to load SPN intel update: %w", err) } @@ -64,8 +67,15 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) { return fmt.Errorf("failed to parse SPN intel update: %w", err) } + // Apply new intel. setVirtualNetworkConfig(intel.VirtualNetworks) - return navigator.Main.UpdateIntel(intel, cfgOptionTrustNodeNodes()) + err = navigator.Main.UpdateIntel(intel, cfgOptionTrustNodeNodes()) + if err != nil { + return fmt.Errorf("failed to update intel on map: %w", err) + } + + intelResource = file + return nil } func resetSPNIntel() { From 9f148f9ea31f86ae27e17f9bfabaf0150fc1504e Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 7 Nov 2024 16:20:23 +0100 Subject: [PATCH 56/62] Improve updater notifications --- service/updates/module.go | 104 ++++++++++++++++++++++++++----------- service/updates/signing.go | 29 ----------- 2 files changed, 75 insertions(+), 58 deletions(-) delete mode 100644 service/updates/signing.go diff --git a/service/updates/module.go b/service/updates/module.go index 607a5512..8b04d3db 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -21,6 +21,7 @@ const ( updateTaskRepeatDuration = 1 * time.Hour noNewUpdateNotificationID = "updates:no-new-update" updateAvailableNotificationID = "updates:update-available" + restartRequiredNotificationID = "updates:restart-required" updateFailedNotificationID = "updates:update-failed" corruptInstallationNotificationID = "updates:corrupt-installation" @@ -216,11 +217,19 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV if err := index.ShouldUpgradeTo(downloader.index); err != nil { log.Infof("updates/%s: no new or eligible update: %s", u.cfg.Name, err) if u.cfg.Notify && u.instance.Notifications() != nil { - u.instance.Notifications().NotifyInfo( - noNewUpdateNotificationID, - "No Updates Available", - "Portmaster v"+u.index.Version+" is the newest version.", - ) + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: noNewUpdateNotificationID, + Type: notifications.Info, + Title: "Portmaster Is Up-To-Date", + Message: "Portmaster v" + index.Version + " is the newest version.", + Expires: time.Now().Add(1 * time.Minute).Unix(), + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "OK", + }, + }, + }) } return ErrNoUpdateAvailable } @@ -229,20 +238,27 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Check if automatic downloads are enabled. if !u.cfg.AutoDownload && !forceApply { if u.cfg.Notify && u.instance.Notifications() != nil { - u.instance.Notifications().NotifyInfo( - updateAvailableNotificationID, - "New Update", - "Portmaster v"+downloader.index.Version+" is available. Click Upgrade to download and upgrade now.", - notifications.Action{ - ID: "upgrade", - Text: "Upgrade Now", - Type: notifications.ActionTypeWebhook, - Payload: notifications.ActionTypeWebhookPayload{ - Method: "POST", - URL: "updates/apply", + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: updateAvailableNotificationID, + Type: notifications.Info, + Title: "New Update Available", + Message: "Portmaster v" + downloader.index.Version + " is available. Click Upgrade to download and upgrade now.", + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "OK", + }, + { + ID: "upgrade", + Text: "Upgrade Now", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, }, }, - ) + }) } return fmt.Errorf("%w: apply updates to download and upgrade", ErrActionRequired) } @@ -267,20 +283,27 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Notify the user that an upgrade is available. if !u.cfg.AutoApply && !forceApply { if u.cfg.Notify && u.instance.Notifications() != nil { - u.instance.Notifications().NotifyInfo( - updateAvailableNotificationID, - "New Update", - "Portmaster v"+downloader.index.Version+" is available. Click Upgrade to upgrade now.", - notifications.Action{ - ID: "upgrade", - Text: "Upgrade Now", - Type: notifications.ActionTypeWebhook, - Payload: notifications.ActionTypeWebhookPayload{ - Method: "POST", - URL: "updates/apply", + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: updateAvailableNotificationID, + Type: notifications.Info, + Title: "New Update Ready", + Message: "Portmaster v" + downloader.index.Version + " is available. Click Upgrade to upgrade now.", + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "OK", + }, + { + ID: "upgrade", + Text: "Upgrade Now", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, }, }, - ) + }) } return fmt.Errorf("%w: apply updates to download and upgrade", ErrActionRequired) } @@ -307,6 +330,29 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Notify user that a restart is required. if u.cfg.Notify && u.instance.Notifications() != nil { + + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: restartRequiredNotificationID, + Type: notifications.Info, + Title: "Restart Required", + Message: "Portmaster v" + downloader.index.Version + " is installed. Restart to use new version.", + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "Later", + }, + { + ID: "restart", + Text: "Restart Now", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, + }, + }, + }) + u.instance.Notifications().NotifyInfo( updateAvailableNotificationID, "Restart Required", diff --git a/service/updates/signing.go b/service/updates/signing.go deleted file mode 100644 index 71e66a5f..00000000 --- a/service/updates/signing.go +++ /dev/null @@ -1,29 +0,0 @@ -package updates - -import "github.com/safing/jess" - -var ( - // BinarySigningKeys holds the signing keys in text format. - BinarySigningKeys = []string{ - // Safing Code Signing Key #1 - "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", - // Safing Code Signing Key #2 - "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", - } - - // BinarySigningTrustStore is an in-memory trust store with the signing keys. - BinarySigningTrustStore = jess.NewMemTrustStore() -) - -func init() { - for _, signingKey := range BinarySigningKeys { - rcpt, err := jess.RecipientFromTextFormat(signingKey) - if err != nil { - panic(err) - } - err = BinarySigningTrustStore.StoreSignet(rcpt) - if err != nil { - panic(err) - } - } -} From c22814e6e10838dc1bef569154954fb6eb02ea71 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 7 Nov 2024 16:20:58 +0100 Subject: [PATCH 57/62] Improve start and shutdown controls and flow --- cmds/portmaster-core/recover_linux.go | 17 +- cmds/portmaster-core/run.go | 99 ++++++---- cmds/portmaster-core/run_linux.go | 143 +++++++------- cmds/portmaster-core/run_windows.go | 262 ++++++++++++++++---------- cmds/portmaster-core/update.go | 5 +- service/instance.go | 59 ++++-- spn/access/module.go | 4 +- 7 files changed, 355 insertions(+), 234 deletions(-) diff --git a/cmds/portmaster-core/recover_linux.go b/cmds/portmaster-core/recover_linux.go index 8ce989ba..6f5532c2 100644 --- a/cmds/portmaster-core/recover_linux.go +++ b/cmds/portmaster-core/recover_linux.go @@ -8,17 +8,28 @@ import ( "strings" "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" "github.com/safing/portmaster/service/firewall/interception" ) -var recoverIPTables bool +var ( + recoverCmd = &cobra.Command{ + Use: "recover-iptables", + Short: "Force an update of all components.", + RunE: update, + } + + recoverIPTables bool +) func init() { - flag.BoolVar(&recoverIPTables, "recover-iptables", false, "recovers ip table rules") + rootCmd.AddCommand(recoverCmd) + + flag.BoolVar(&recoverIPTables, "recover-iptables", false, "recovers ip table rules (backward compatibility; use command instead)") } -func recoverIPTablesCmd() error { +func recover(cmd *cobra.Command, args []string) error { // interception.DeactiveNfqueueFirewall uses coreos/go-iptables // which shells out to the /sbin/iptables binary. As a result, // we don't get the errno of the actual error and need to parse the diff --git a/cmds/portmaster-core/run.go b/cmds/portmaster-core/run.go index 0cbe4812..4c11a1cd 100644 --- a/cmds/portmaster-core/run.go +++ b/cmds/portmaster-core/run.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "errors" "flag" "fmt" @@ -9,44 +8,43 @@ import ( "log/slog" "os" "runtime/pprof" - "syscall" + "time" "github.com/spf13/cobra" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/spn/conf" ) -var ( - printStackOnExit bool - enableInputSignals bool - - sigUSR1 = syscall.Signal(0xa) // dummy for windows -) +var printStackOnExit bool func init() { flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") - flag.BoolVar(&enableInputSignals, "input-signals", false, "emulate signals using stdin") +} + +type SystemService interface { + Run() + IsService() bool + RestartService() error } func cmdRun(cmd *cobra.Command, args []string) { - // Call platform specific checks, that will execute commands like "recover-iptables" - platformSpecificChecks() + // Run platform specific setup or switches. + runPlatformSpecifics(cmd, args) + + // SETUP svcCfg.VerifyBinaryUpdates = nil // FIXME svcCfg.VerifyIntelUpdates = nil // FIXME - instance := createInstance() - run(instance) -} - -func createInstance() *service.Instance { // enable SPN client mode conf.EnableClient(true) conf.EnableIntegration(true) // Create instance. + // Instance modules might request a cmdline execution of a function. var execCmdLine bool instance, err := service.New(svcCfg) switch { @@ -76,7 +74,56 @@ func createInstance() *service.Instance { } os.Exit(0) } - return instance + + // START + + // Set default log level and start logging. + log.SetLogLevel(log.WarningLevel) + _ = log.Start() + + // Create system service. + service := NewSystemService(instance) + + // Start instance via system service manager. + go func() { + service.Run() + }() + + // SHUTDOWN + + // Wait for shutdown to be started. + <-instance.ShuttingDown() + + // Wait for shutdown to be finished. + select { + case <-instance.ShutdownComplete(): + case <-time.After(3 * time.Minute): + printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") + } + + // Stop logging. + log.Shutdown() + + // Print stack on shutdown, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON EXIT") + } + + // Check if restart was triggered and send start service command if true. + if instance.ShouldRestart && service.IsService() { + if err := service.RestartService(); err != nil { + slog.Error("failed to restart service", "err", err) + } + } + + // Give a small amount of time for everything to settle: + // - All logs written. + // - Restart command started, if needed. + // - Windows service manager notified. + time.Sleep(100 * time.Millisecond) + + // Exit + os.Exit(instance.ExitCode()) } func printStackTo(writer io.Writer, msg string) { @@ -88,21 +135,3 @@ func printStackTo(writer io.Writer, msg string) { slog.Error("failed to write stack trace", "err", err) } } - -func inputSignals(signalCh chan os.Signal) { - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - switch scanner.Text() { - case "SIGHUP": - signalCh <- syscall.SIGHUP - case "SIGINT": - signalCh <- syscall.SIGINT - case "SIGQUIT": - signalCh <- syscall.SIGQUIT - case "SIGTERM": - signalCh <- syscall.SIGTERM - case "SIGUSR1": - signalCh <- sigUSR1 - } - } -} diff --git a/cmds/portmaster-core/run_linux.go b/cmds/portmaster-core/run_linux.go index 25c6da0b..7859ed26 100644 --- a/cmds/portmaster-core/run_linux.go +++ b/cmds/portmaster-core/run_linux.go @@ -7,39 +7,37 @@ import ( "os/exec" "os/signal" "syscall" - "time" processInfo "github.com/shirou/gopsutil/process" + "github.com/spf13/cobra" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service" ) -func run(instance *service.Instance) { - // Set default log level. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() +type LinuxSystemService struct { + instance *service.Instance +} - // Start - go func() { - err := instance.Start() - if err != nil { - fmt.Printf("instance start failed: %s\n", err) +func NewSystemService(instance *service.Instance) *LinuxSystemService { + return &LinuxSystemService{instance: instance} +} - // Print stack on start failure, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE") - } +func (s *LinuxSystemService) Run() { + // Start instance. + err := s.instance.Start() + if err != nil { + slog.Error("failed to start", "err", err) - os.Exit(1) + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stderr, "PRINTING STACK ON START FAILURE") } - }() - // Wait for signal. - signalCh := make(chan os.Signal, 1) - if enableInputSignals { - go inputSignals(signalCh) + os.Exit(1) } + + // Subscribe to signals. + signalCh := make(chan os.Signal, 1) signal.Notify( signalCh, os.Interrupt, @@ -47,74 +45,61 @@ func run(instance *service.Instance) { syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, - sigUSR1, + syscall.SIGUSR1, ) - select { - case sig := <-signalCh: - // Only print and continue to wait if SIGUSR1 - if sig == sigUSR1 { - printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") - } else { - fmt.Println(" ") // CLI output. - slog.Warn("program was interrupted, stopping") + // Wait for shutdown signal. +wait: + for { + select { + case sig := <-signalCh: + // Only print and continue to wait if SIGUSR1 + if sig == syscall.SIGUSR1 { + printStackTo(os.Stdout, "PRINTING STACK ON REQUEST") + continue wait + } else { + // Trigger shutdown. + fmt.Printf(" ", sig) // CLI output. + slog.Warn("received stop signal", "signal", sig) + s.instance.Shutdown() + break wait + } + case <-s.instance.ShuttingDown(): + break wait } - - case <-instance.Stopped(): - log.Shutdown() - os.Exit(instance.ExitCode()) } + // Wait for shutdown to finish. + // Catch signals during shutdown. - // Rapid unplanned disassembly after 5 interrupts. - go func() { - forceCnt := 5 - for { - <-signalCh - forceCnt-- - if forceCnt > 0 { - fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) - } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") - os.Exit(1) + // Force exit after 5 interrupts. + forceCnt := 5 + for { + select { + case <-s.instance.ShutdownComplete(): + return + case sig := <-signalCh: + if sig != syscall.SIGUSR1 { + forceCnt-- + if forceCnt > 0 { + fmt.Printf(" again, but already shutting down - %d more to force\n", sig, forceCnt) + } else { + printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + os.Exit(1) + } } } - }() - - // Rapid unplanned disassembly after 3 minutes. - go func() { - time.Sleep(3 * time.Minute) - printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") - os.Exit(1) - }() - - // Stop instance. - if err := instance.Stop(); err != nil { - slog.Error("failed to stop", "err", err) } - log.Shutdown() - - // Print stack on shutdown, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON EXIT") - } - - // Check if restart was trigger and send start service command if true. - if isRunningAsService() && instance.ShouldRestart { - _ = runServiceRestart() - } - - os.Exit(instance.ExitCode()) } -func runServiceRestart() error { +func (s *LinuxSystemService) RestartService() error { // Check if user defined custom command for restarting the service. restartCommand, exists := os.LookupEnv("PORTMASTER_RESTART_COMMAND") // Run the service restart var cmd *exec.Cmd if exists && restartCommand != "" { - log.Debugf(`instance: running custom restart command: "%s"`, restartCommand) + slog.Debug("running custom restart command", "command", restartCommand) cmd = exec.Command("sh", "-c", restartCommand) } else { cmd = exec.Command("systemctl", "restart", "portmaster") @@ -125,28 +110,30 @@ func runServiceRestart() error { return nil } -func isRunningAsService() bool { - // Get the current process ID +func (s *LinuxSystemService) IsService() bool { + // Get own process ID pid := os.Getpid() + // Get parent process ID. currentProcess, err := processInfo.NewProcess(int32(pid)) //nolint:gosec if err != nil { return false } - ppid, err := currentProcess.Ppid() if err != nil { return false } + // Check if the parent process ID is 1 == init system return ppid == 1 } -func platformSpecificChecks() { - // If flag is set. Run recover IP tables and exit. (Can be true only on linux) +func runPlatformSpecifics(cmd *cobra.Command, args []string) { + // If recover-iptables flag is set, run the recover-iptables command. + // This is for backwards compatibility if recoverIPTables { exitCode := 0 - err := recoverIPTablesCmd() + err := recover(cmd, args) if err != nil { fmt.Printf("failed: %s", err) exitCode = 1 diff --git a/cmds/portmaster-core/run_windows.go b/cmds/portmaster-core/run_windows.go index 8e0d9a6d..3976745f 100644 --- a/cmds/portmaster-core/run_windows.go +++ b/cmds/portmaster-core/run_windows.go @@ -12,138 +12,165 @@ import ( "os/exec" "os/signal" "syscall" - "time" - "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service" + "github.com/spf13/cobra" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" ) const serviceName = "PortmasterCore" -type windowsService struct { +type WindowsSystemService struct { instance *service.Instance } -func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { - const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown - changes <- svc.Status{State: svc.StartPending} - ws.instance.Start() - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} +func NewSystemService(instance *service.Instance) *WindowsSystemService { + return &WindowsSystemService{instance: instance} +} -service: +func (s *WindowsSystemService) Run() { + svcRun := svc.Run + + // Check if we are running interactively. + isService, err := svc.IsWindowsService() + switch { + case err != nil: + slog.Warn("failed to determine if running interactively", "err", err) + slog.Warn("continuing without service integration (no real service)") + svcRun = debug.Run + + case !isService: + slog.Warn("running interactively, switching to debug execution (no real service)") + svcRun = debug.Run + } + + // Run service client. + err = svcRun(serviceName, s) + if err != nil { + slog.Error("service execution failed", "err", err) + os.Exit(1) + } + + // Execution continues in s.Execute(). +} + +func (s *WindowsSystemService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { + // Tell service manager we are starting. + changes <- svc.Status{State: svc.StartPending} + + // Start instance. + err := s.instance.Start() + if err != nil { + fmt.Printf("failed to start: %s\n", err) + + // Print stack on start failure, if enabled. + if printStackOnExit { + printStackTo(os.Stderr, "PRINTING STACK ON START FAILURE") + } + + // Notify service manager we stopped again. + changes <- svc.Status{State: svc.Stopped} + + // Relay exit code to service manager. + return false, 1 + } + + // Tell service manager we are up and running! + changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} + + // Subscribe to signals. + // Docs: https://pkg.go.dev/os/signal?GOOS=windows + signalCh := make(chan os.Signal, 4) + signal.Notify( + signalCh, + + // Windows ^C (Control-C) or ^BREAK (Control-Break). + // Completely prevents kill. + os.Interrupt, + + // Windows CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT. + // Does not prevent kill, but gives a little time to stop service. + syscall.SIGTERM, + ) + + // Wait for shutdown signal. +waitSignal: for { select { - case <-ws.instance.Stopped(): - log.Infof("instance stopped") - break service + case sig := <-signalCh: + // Trigger shutdown. + fmt.Printf(" ", sig) // CLI output. + slog.Warn("received stop signal", "signal", sig) + break waitSignal + case c := <-changeRequests: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: - log.Debugf("received shutdown command") - changes <- svc.Status{State: svc.StopPending} - ws.instance.Shutdown() + fmt.Printf(" ", serviceCmdName(c.Cmd)) // CLI output. + slog.Warn("received service shutdown command", "cmd", c.Cmd) + break waitSignal + default: - log.Errorf("unexpected control request: %+v", c) + slog.Error("unexpected service control request", "cmd", serviceCmdName(c.Cmd)) } + + case <-s.instance.ShuttingDown(): + break waitSignal } } - log.Shutdown() - - // send stopped status - changes <- svc.Status{State: svc.Stopped} - // wait a little for the status to reach Windows - time.Sleep(100 * time.Millisecond) - - return ssec, errno -} - -func run(instance *service.Instance) error { - log.SetLogLevel(log.WarningLevel) - _ = log.Start() - - // check if we are running interactively - isService, err := svc.IsWindowsService() - if err != nil { - return fmt.Errorf("could not determine if running interactively: %s", err) - } - - // select service run type - svcRun := svc.Run - if !isService { - log.Warningf("running interactively, switching to debug execution (no real service).") - svcRun = debug.Run - go registerSignalHandler(instance) - } - - // run service client - sErr := svcRun(serviceName, &windowsService{ - instance: instance, - }) - if sErr != nil { - fmt.Printf("shuting down service with error: %s", sErr) - } else { - fmt.Printf("shuting down service") - } - - // Check if restart was trigger and send start service command if true. - if isRunningAsService() && instance.ShouldRestart { - _ = runServiceRestart() - } - - return err -} - -func registerSignalHandler(instance *service.Instance) { - // Wait for signal. - signalCh := make(chan os.Signal, 1) - if enableInputSignals { - go inputSignals(signalCh) - } - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - sigUSR1, - ) - - select { - case sig := <-signalCh: - // Only print and continue to wait if SIGUSR1 - if sig == sigUSR1 { - printStackTo(os.Stderr, "PRINTING STACK ON REQUEST") - } else { - fmt.Println(" ") // CLI output. - slog.Warn("program was interrupted, stopping") - instance.Shutdown() - } - } + // Wait for shutdown to finish. + changes <- svc.Status{State: svc.StopPending} // Catch signals during shutdown. - // Rapid unplanned disassembly after 5 interrupts. - go func() { - forceCnt := 5 - for { - <-signalCh + // Force exit after 5 interrupts. + forceCnt := 5 +waitShutdown: + for { + select { + case <-s.instance.ShutdownComplete(): + break waitShutdown + + case sig := <-signalCh: forceCnt-- if forceCnt > 0 { - fmt.Printf(" again, but already shutting down - %d more to force\n", forceCnt) + fmt.Printf(" but already shutting down - %d more to force\n", sig, forceCnt) } else { printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") os.Exit(1) } + + case c := <-changeRequests: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + + case svc.Stop, svc.Shutdown: + forceCnt-- + if forceCnt > 0 { + fmt.Printf(" but already shutting down - %d more to force\n", serviceCmdName(c.Cmd), forceCnt) + } else { + printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + os.Exit(1) + } + + default: + slog.Error("unexpected service control request", "cmd", serviceCmdName(c.Cmd)) + } } - }() + } + + // Notify service manager. + changes <- svc.Status{State: svc.Stopped} + + return false, 0 } -func isRunningAsService() bool { +func (s *WindowsSystemService) IsService() bool { isService, err := svc.IsWindowsService() if err != nil { return false @@ -151,7 +178,7 @@ func isRunningAsService() bool { return isService } -func runServiceRestart() error { +func (s *WindowsSystemService) RestartService() error { // Script that wait for portmaster service status to change to stop // and then sends a start command for the same service. command := ` @@ -172,4 +199,41 @@ sc.exe start $serviceName` return nil } -func platformSpecificChecks() {} +func runPlatformSpecifics(cmd *cobra.Command, args []string) + +func serviceCmdName(cmd svc.Cmd) string { + switch cmd { + case svc.Stop: + return "Stop" + case svc.Pause: + return "Pause" + case svc.Continue: + return "Continue" + case svc.Interrogate: + return "Interrogate" + case svc.Shutdown: + return "Shutdown" + case svc.ParamChange: + return "ParamChange" + case svc.NetBindAdd: + return "NetBindAdd" + case svc.NetBindRemove: + return "NetBindRemove" + case svc.NetBindEnable: + return "NetBindEnable" + case svc.NetBindDisable: + return "NetBindDisable" + case svc.DeviceEvent: + return "DeviceEvent" + case svc.HardwareProfileChange: + return "HardwareProfileChange" + case svc.PowerEvent: + return "PowerEvent" + case svc.SessionChange: + return "SessionChange" + case svc.PreShutdown: + return "PreShutdown" + default: + return "Unknown Command" + } +} diff --git a/cmds/portmaster-core/update.go b/cmds/portmaster-core/update.go index ceb0ff6c..a3158dea 100644 --- a/cmds/portmaster-core/update.go +++ b/cmds/portmaster-core/update.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log/slog" "github.com/spf13/cobra" @@ -53,11 +54,11 @@ func update(cmd *cobra.Command, args []string) error { // Force update all. binErr := binaryUpdates.ForceUpdate() if binErr != nil { - log.Errorf("binary update failed: %s", binErr) + slog.Error("binary update failed", "err", binErr) } intelErr := intelUpdates.ForceUpdate() if intelErr != nil { - log.Errorf("intel update failed: %s", intelErr) + slog.Error("intel update failed", "err", intelErr) } // Return error. diff --git a/service/instance.go b/service/instance.go index 5bfeb7a1..8fc9a17c 100644 --- a/service/instance.go +++ b/service/instance.go @@ -49,8 +49,12 @@ import ( // Instance is an instance of a Portmaster service. type Instance struct { - ctx context.Context - cancelCtx context.CancelFunc + ctx context.Context + cancelCtx context.CancelFunc + + shutdownCtx context.Context + cancelShutdownCtx context.CancelFunc + serviceGroup *mgr.Group binDir string @@ -607,20 +611,13 @@ func (i *Instance) Ready() bool { return i.serviceGroup.Ready() } -// Ctx returns the instance context. -// It is only canceled on shutdown. -func (i *Instance) Ctx() context.Context { - return i.ctx -} - -// Start starts the instance. +// Start starts the instance modules. func (i *Instance) Start() error { return i.serviceGroup.Start() } -// Stop stops the instance and cancels the instance context when done. +// Stop stops the instance modules. func (i *Instance) Stop() error { - defer i.cancelCtx() return i.serviceGroup.Stop() } @@ -649,9 +646,19 @@ func (i *Instance) Shutdown() { } func (i *Instance) shutdown(exitCode int) { + // Only shutdown once. + if i.IsShuttingDown() { + return + } + // Set given exit code. i.exitCode.Store(int32(exitCode)) + // Cancel contexts. + i.cancelCtx() + defer i.cancelShutdownCtx() + + // Start shutdown asynchronously in a separate manager. m := mgr.New("instance") m.Go("shutdown", func(w *mgr.WorkerCtx) error { for { @@ -665,16 +672,38 @@ func (i *Instance) shutdown(exitCode int) { }) } -// Stopping returns whether the instance is shutting down. -func (i *Instance) Stopping() bool { +// Ctx returns the instance context. +// It is canceled when shutdown is started. +func (i *Instance) Ctx() context.Context { + return i.ctx +} + +// IsShuttingDown returns whether the instance is shutting down. +func (i *Instance) IsShuttingDown() bool { return i.ctx.Err() != nil } -// Stopped returns a channel that is triggered when the instance has shut down. -func (i *Instance) Stopped() <-chan struct{} { +// ShuttingDown returns a channel that is triggered when the instance starts shutting down. +func (i *Instance) ShuttingDown() <-chan struct{} { return i.ctx.Done() } +// ShutdownCtx returns the instance shutdown context. +// It is canceled when shutdown is complete. +func (i *Instance) ShutdownCtx() context.Context { + return i.shutdownCtx +} + +// IsShutDown returns whether the instance has stopped. +func (i *Instance) IsShutDown() bool { + return i.shutdownCtx.Err() != nil +} + +// ShutDownComplete returns a channel that is triggered when the instance has shut down. +func (i *Instance) ShutdownComplete() <-chan struct{} { + return i.shutdownCtx.Done() +} + // ExitCode returns the set exit code of the instance. func (i *Instance) ExitCode() int { return int(i.exitCode.Load()) diff --git a/spn/access/module.go b/spn/access/module.go index d49524f9..a805240a 100644 --- a/spn/access/module.go +++ b/spn/access/module.go @@ -80,7 +80,7 @@ func start() error { // Add config listener to enable/disable SPN. module.instance.Config().EventConfigChange.AddCallback("spn enable check", func(wc *mgr.WorkerCtx, s struct{}) (bool, error) { // Do not do anything when we are shutting down. - if module.instance.Stopping() { + if module.instance.IsShuttingDown() { return true, nil } @@ -255,5 +255,5 @@ func New(instance instance) (*Access, error) { type instance interface { Config() *config.Config SPNGroup() *mgr.ExtendedGroup - Stopping() bool + IsShuttingDown() bool } From 34894ea7d698006a8a6d839ee1e58f23b2294ade Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 Nov 2024 14:38:36 +0100 Subject: [PATCH 58/62] Add sign command to updatemgr --- cmds/updatemgr/sign.go | 180 +++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 cmds/updatemgr/sign.go diff --git a/cmds/updatemgr/sign.go b/cmds/updatemgr/sign.go new file mode 100644 index 00000000..5f6d8b31 --- /dev/null +++ b/cmds/updatemgr/sign.go @@ -0,0 +1,180 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/safing/jess" + "github.com/safing/jess/filesig" + "github.com/safing/jess/truststores" + "github.com/safing/portmaster/service/updates" +) + +func init() { + rootCmd.AddCommand(signCmd) + + // Required argument: envelope + signCmd.Flags().StringVarP(&envelopeName, "envelope", "", "", + "specify envelope name used for signing", + ) + _ = signCmd.MarkFlagRequired("envelope") + + // Optional arguments: verbose, tsdir, tskeyring + signCmd.Flags().BoolVarP(&signVerbose, "verbose", "v", false, + "enable verbose output", + ) + signCmd.Flags().StringVarP(&trustStoreDir, "tsdir", "", "", + "specify a truststore directory (default loaded from JESS_TS_DIR env variable)", + ) + signCmd.Flags().StringVarP(&trustStoreKeyring, "tskeyring", "", "", + "specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir", + ) +} + +var ( + signCmd = &cobra.Command{ + Use: "sign [index.json file]", + Short: "Sign an index", + RunE: sign, + Args: cobra.ExactArgs(1), + } + + envelopeName string + signVerbose bool +) + +func sign(cmd *cobra.Command, args []string) error { + indexFilename := args[0] + + // Setup trust store. + trustStore, err := setupTrustStore() + if err != nil { + return err + } + + // Get envelope. + signingEnvelope, err := trustStore.GetEnvelope(envelopeName) + if err != nil { + return err + } + + // Read index file from disk. + unsignedIndexData, err := os.ReadFile(indexFilename) + if err != nil { + return fmt.Errorf("read index file: %w", err) + } + + // Parse index and check if it is valid. + index, err := updates.ParseIndex(unsignedIndexData, nil) + if err != nil { + return fmt.Errorf("invalid index: %w", err) + } + err = index.CanDoUpgrades() + if err != nil { + return fmt.Errorf("invalid index: %w", err) + } + + // Sign index. + signedIndexData, err := filesig.AddJSONSignature(unsignedIndexData, signingEnvelope, trustStore) + if err != nil { + return fmt.Errorf("sign: %w", err) + } + + // Check by parsing again. + index, err = updates.ParseIndex(signedIndexData, nil) + if err != nil { + return fmt.Errorf("invalid index after signing: %w", err) + } + err = index.CanDoUpgrades() + if err != nil { + return fmt.Errorf("invalid index after signing: %w", err) + } + + // Write back to file. + err = os.WriteFile(indexFilename, signedIndexData, 0o0644) + if err != nil { + return fmt.Errorf("write signed index file: %w", err) + } + + return nil +} + +var ( + trustStoreDir string + trustStoreKeyring string +) + +func setupTrustStore() (trustStore truststores.ExtendedTrustStore, err error) { + // Get trust store directory. + if trustStoreDir == "" { + trustStoreDir, _ = os.LookupEnv("JESS_TS_DIR") + if trustStoreDir == "" { + trustStoreDir, _ = os.LookupEnv("JESS_TSDIR") + } + } + if trustStoreDir != "" { + trustStore, err = truststores.NewDirTrustStore(trustStoreDir) + if err != nil { + return nil, err + } + } + + // Get trust store keyring. + if trustStore == nil { + if trustStoreKeyring == "" { + trustStoreKeyring, _ = os.LookupEnv("JESS_TS_KEYRING") + if trustStoreKeyring == "" { + trustStoreKeyring, _ = os.LookupEnv("JESS_TSKEYRING") + } + } + if trustStoreKeyring != "" { + trustStore, err = truststores.NewKeyringTrustStore(trustStoreKeyring) + if err != nil { + return nil, err + } + } + } + + // Truststore is mandatory. + if trustStore == nil { + return nil, errors.New("no truststore configured, please pass arguments or use env variables") + } + + return trustStore, nil +} + +func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string { + signedBy := make([]string, 0, len(fds)) + for _, fd := range fds { + if sig := fd.Signature(); sig != nil { + for _, seal := range sig.Signatures { + if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { + signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) + } else { + signedBy = append(signedBy, seal.ID) + } + } + } + } + return strings.Join(signedBy, " and ") +} + +func getSignedBySingle(fd *filesig.FileData, trustStore jess.TrustStore) string { + if sig := fd.Signature(); sig != nil { + signedBy := make([]string, 0, len(sig.Signatures)) + for _, seal := range sig.Signatures { + if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { + signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) + } else { + signedBy = append(signedBy, seal.ID) + } + } + return strings.Join(signedBy, " and ") + } + + return "" +} diff --git a/go.mod b/go.mod index 9590ae56..a97838af 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/oschwald/maxminddb-golang v1.13.1 github.com/r3labs/diff/v3 v3.0.1 github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5 - github.com/safing/jess v0.3.4 + github.com/safing/jess v0.3.5 github.com/safing/structures v1.1.0 github.com/seehuhn/fortuna v1.0.1 github.com/shirou/gopsutil v3.21.11+incompatible diff --git a/go.sum b/go.sum index fbd91dd0..8f65cf10 100644 --- a/go.sum +++ b/go.sum @@ -250,6 +250,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safing/jess v0.3.4 h1:/p6ensqEUn2jI/z1EB9JUdwH4MJQirh/C9jEwNBzxw8= github.com/safing/jess v0.3.4/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI= +github.com/safing/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU= +github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI= github.com/safing/structures v1.1.0 h1:QzHBQBjaZSLzw2f6PM4ibSmPcfBHAOB5CKJ+k4FYkhQ= github.com/safing/structures v1.1.0/go.mod h1:QUrB74FcU41ahQ5oy3YNFCoSq+twE/n3+vNZc2K35II= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= From 2d0d7119382788f9c74640047d5b7fd664ead567 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 Nov 2024 14:39:28 +0100 Subject: [PATCH 59/62] Improve starting and stopping --- cmds/portmaster-core/main.go | 1 + cmds/portmaster-core/run.go | 21 +++++++++------------ cmds/portmaster-core/run_linux.go | 4 ++-- cmds/portmaster-core/update.go | 2 -- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index eb677a47..993ffb55 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -29,6 +29,7 @@ var ( func init() { // Add Go's default flag set. + // TODO: Move flags throughout Portmaster to here and add their values to the service config. rootCmd.Flags().AddGoFlagSet(flag.CommandLine) // Add persisent flags for all commands. diff --git a/cmds/portmaster-core/run.go b/cmds/portmaster-core/run.go index 4c11a1cd..33702992 100644 --- a/cmds/portmaster-core/run.go +++ b/cmds/portmaster-core/run.go @@ -36,10 +36,8 @@ func cmdRun(cmd *cobra.Command, args []string) { // SETUP - svcCfg.VerifyBinaryUpdates = nil // FIXME - svcCfg.VerifyIntelUpdates = nil // FIXME - - // enable SPN client mode + // Enable SPN client mode. + // TODO: Move this to service config. conf.EnableClient(true) conf.EnableIntegration(true) @@ -97,18 +95,14 @@ func cmdRun(cmd *cobra.Command, args []string) { // Wait for shutdown to be finished. select { case <-instance.ShutdownComplete(): + // Print stack on shutdown, if enabled. + if printStackOnExit { + printStackTo(os.Stdout, "PRINTING STACK ON EXIT") + } case <-time.After(3 * time.Minute): printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") } - // Stop logging. - log.Shutdown() - - // Print stack on shutdown, if enabled. - if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON EXIT") - } - // Check if restart was triggered and send start service command if true. if instance.ShouldRestart && service.IsService() { if err := service.RestartService(); err != nil { @@ -116,6 +110,9 @@ func cmdRun(cmd *cobra.Command, args []string) { } } + // Stop logging. + log.Shutdown() + // Give a small amount of time for everything to settle: // - All logs written. // - Restart command started, if needed. diff --git a/cmds/portmaster-core/run_linux.go b/cmds/portmaster-core/run_linux.go index 7859ed26..9b65b804 100644 --- a/cmds/portmaster-core/run_linux.go +++ b/cmds/portmaster-core/run_linux.go @@ -52,6 +52,8 @@ func (s *LinuxSystemService) Run() { wait: for { select { + case <-s.instance.ShuttingDown(): + break wait case sig := <-signalCh: // Only print and continue to wait if SIGUSR1 if sig == syscall.SIGUSR1 { @@ -64,8 +66,6 @@ wait: s.instance.Shutdown() break wait } - case <-s.instance.ShuttingDown(): - break wait } } diff --git a/cmds/portmaster-core/update.go b/cmds/portmaster-core/update.go index a3158dea..31fcb881 100644 --- a/cmds/portmaster-core/update.go +++ b/cmds/portmaster-core/update.go @@ -24,8 +24,6 @@ func init() { func update(cmd *cobra.Command, args []string) error { // Finalize config. - svcCfg.VerifyBinaryUpdates = nil // FIXME - svcCfg.VerifyIntelUpdates = nil // FIXME err := svcCfg.Init() if err != nil { return fmt.Errorf("internal configuration error: %w", err) From ddf7ba170ea0d297d0b89e25ff21ccd932b4ff4d Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 Nov 2024 14:39:58 +0100 Subject: [PATCH 60/62] Fix tests and issues --- base/config/init_test.go | 14 ++-- cmds/updatemgr/scan.go | 6 +- service/instance.go | 22 +++--- service/updates/downloader.go | 15 ++-- service/updates/index.go | 38 +++++----- service/updates/index_scan.go | 103 +++---------------------- service/updates/module.go | 98 +++++++++++++++++------- service/updates/updates_test.go | 128 ++++++++++++++++++++++++-------- service/updates/upgrade.go | 43 ++++++----- spn/instance.go | 89 ++++++++++++++++------ 10 files changed, 325 insertions(+), 231 deletions(-) diff --git a/base/config/init_test.go b/base/config/init_test.go index e3390379..d5d90aea 100644 --- a/base/config/init_test.go +++ b/base/config/init_test.go @@ -29,21 +29,23 @@ func newTestInstance(testName string) (*testInstance, error) { }, nil } -func TestConfigPersistence(t *testing.T) { - t.Parallel() - +func TestMain(m *testing.M) { instance, err := newTestInstance("test-config") if err != nil { - t.Fatalf("failed to create test instance: %s", err) + panic(fmt.Errorf("failed to create test instance: %w", err)) } defer func() { _ = os.RemoveAll(instance.DataDir()) }() module, err = New(instance) if err != nil { - t.Fatalf("failed to initialize module: %s", err) + panic(fmt.Errorf("failed to initialize module: %w", err)) } - err = SaveConfig() + m.Run() +} + +func TestConfigPersistence(t *testing.T) { //nolint:paralleltest + err := SaveConfig() if err != nil { t.Fatal(err) } diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go index db60058f..9ef29f15 100644 --- a/cmds/updatemgr/scan.go +++ b/cmds/updatemgr/scan.go @@ -70,16 +70,16 @@ func init() { } func scan(cmd *cobra.Command, args []string) error { - bundle, err := updates.GenerateIndexFromDir(scanDir, scanConfig) + index, err := updates.GenerateIndexFromDir(scanDir, scanConfig) if err != nil { return err } - bundleStr, err := json.MarshalIndent(&bundle, "", " ") + indexJson, err := json.MarshalIndent(&index, "", " ") if err != nil { return fmt.Errorf("marshal index: %w", err) } - fmt.Printf("%s", bundleStr) + fmt.Printf("%s", indexJson) return nil } diff --git a/service/instance.go b/service/instance.go index 8fc9a17c..ddb23671 100644 --- a/service/instance.go +++ b/service/instance.go @@ -130,6 +130,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx dataDir: svcCfg.DataDir, } instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) + instance.shutdownCtx, instance.cancelShutdownCtx = context.WithCancel(context.Background()) // Base modules instance.base, err = base.New(instance) @@ -651,24 +652,23 @@ func (i *Instance) shutdown(exitCode int) { return } + // Cancel main context. + i.cancelCtx() + // Set given exit code. i.exitCode.Store(int32(exitCode)) - // Cancel contexts. - i.cancelCtx() - defer i.cancelShutdownCtx() - // Start shutdown asynchronously in a separate manager. m := mgr.New("instance") m.Go("shutdown", func(w *mgr.WorkerCtx) error { - for { - if err := i.Stop(); err != nil { - w.Error("failed to shutdown", "err", err, "retry", "1s") - time.Sleep(1 * time.Second) - } else { - return nil - } + // Stop all modules. + if err := i.Stop(); err != nil { + w.Error("failed to shutdown", "err", err) } + + // Cancel shutdown process context. + i.cancelShutdownCtx() + return nil }) } diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 8d0d8b24..30c32261 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -56,7 +56,7 @@ func (d *Downloader) updateIndex(ctx context.Context) error { } log.Warningf("updates/%s: failed to update index from %q: %s", d.u.cfg.Name, url, err) - err = fmt.Errorf("update index file from %q: %s", url, err) + err = fmt.Errorf("update index file from %q: %w", url, err) } if err != nil { return fmt.Errorf("all index URLs failed, last error: %w", err) @@ -65,7 +65,7 @@ func (d *Downloader) updateIndex(ctx context.Context) error { // Write the index into a file. indexFilepath := filepath.Join(d.u.cfg.DownloadDirectory, d.u.cfg.IndexFile) - err = os.WriteFile(indexFilepath, []byte(indexData), defaultFileMode) + err = os.WriteFile(indexFilepath, indexData, defaultFileMode) if err != nil { return fmt.Errorf("write index file: %w", err) } @@ -111,7 +111,7 @@ func (d *Downloader) gatherExistingFiles(dir string) error { // Read full file. fileData, err := os.ReadFile(fullpath) if err != nil { - log.Debugf("updates/%s: failed to read file %q while searching for existing files: %w", d.u.cfg.Name, fullpath, err) + log.Debugf("updates/%s: failed to read file %q while searching for existing files: %s", d.u.cfg.Name, fullpath, err) return fmt.Errorf("failed to read file %s: %w", fullpath, err) } @@ -150,7 +150,12 @@ artifacts: if err == nil { continue artifacts } - log.Debugf("updates/%s: failed to copy existing file %s: %w", d.u.cfg.Name, artifact.Filename, err) + log.Debugf("updates/%s: failed to copy existing file %s: %s", d.u.cfg.Name, artifact.Filename, err) + } + + // Check if the artifact has download URLs. + if len(artifact.URLs) == 0 { + return fmt.Errorf("artifact %s is missing download URLs", artifact.Filename) } // Try to download the artifact from one of the URLs. @@ -163,7 +168,7 @@ artifacts: // Valid artifact found! break artifactURLs } - err = fmt.Errorf("update index file from %q: %s", url, err) + err = fmt.Errorf("update index file from %q: %w", url, err) } if err != nil { return fmt.Errorf("all artifact URLs for %s failed, last error: %w", artifact.Filename, err) diff --git a/service/updates/index.go b/service/updates/index.go index f627d0d3..8a30f643 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -15,6 +15,7 @@ import ( "time" semver "github.com/hashicorp/go-version" + "github.com/safing/jess" "github.com/safing/jess/filesig" ) @@ -26,7 +27,7 @@ const currentPlatform = runtime.GOOS + "_" + runtime.GOARCH var zeroVersion = semver.Must(semver.NewVersion("0.0.0")) -// Artifacts represents a single file with metadata. +// Artifact represents a single file with metadata. type Artifact struct { Filename string `json:"Filename"` SHA256 string `json:"SHA256"` @@ -85,7 +86,7 @@ func (a *Artifact) IsNewerThan(b *Artifact) (newer, ok bool) { } func (a *Artifact) export(dir string, indexVersion *semver.Version) *Artifact { - copy := &Artifact{ + copied := &Artifact{ Filename: a.Filename, SHA256: a.SHA256, URLs: a.URLs, @@ -98,20 +99,20 @@ func (a *Artifact) export(dir string, indexVersion *semver.Version) *Artifact { // Make sure we have a version number. switch { - case copy.versionNum != nil: + case copied.versionNum != nil: // Version already parsed. - case copy.Version != "": + case copied.Version != "": // Need to parse version. - v, err := semver.NewVersion(copy.Version) + v, err := semver.NewVersion(copied.Version) if err == nil { - copy.versionNum = v + copied.versionNum = v } default: // No version defined, inherit index version. - copy.versionNum = indexVersion + copied.versionNum = indexVersion } - return copy + return copied } // Index represents a collection of artifacts with metadata. @@ -146,8 +147,8 @@ func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error) } // Parse json. - var index Index - err := json.Unmarshal([]byte(jsonContent), &index) + index := &Index{} + err := json.Unmarshal(jsonContent, index) if err != nil { return nil, fmt.Errorf("parse index: %w", err) } @@ -158,7 +159,7 @@ func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error) return nil, err } - return &index, nil + return index, nil } func (index *Index) init() error { @@ -219,7 +220,7 @@ func (index *Index) ShouldUpgradeTo(newIndex *Index) error { return fmt.Errorf("current index cannot do upgrades: %w", err) } if err := newIndex.CanDoUpgrades(); err != nil { - return fmt.Errorf("new index cannot do upgrade: %w") + return fmt.Errorf("new index cannot do upgrade: %w", err) } switch { @@ -229,13 +230,14 @@ func (index *Index) ShouldUpgradeTo(newIndex *Index) error { return nil case index.Name != newIndex.Name: - return errors.New("index names do not match") - - case index.versionNum.GreaterThan(newIndex.versionNum): - return errors.New("current index has newer version") + return errors.New("new index name does not match") case index.Published.After(newIndex.Published): - return errors.New("current index was published later") + return errors.New("new index is older (time)") + + case index.versionNum.Segments()[0] > newIndex.versionNum.Segments()[0]: + // Downgrades are allowed, if they are not breaking changes. + return errors.New("new index is a breaking change downgrade") case index.Published.Equal(newIndex.Published): // "Do nothing". @@ -252,7 +254,7 @@ func (index *Index) VerifyArtifacts(dir string) error { for _, artifact := range index.Artifacts { err := checkSHA256SumFile(filepath.Join(dir, artifact.Filename), artifact.SHA256) if err != nil { - return fmt.Errorf("verify %s: %s", artifact.Filename, err) + return fmt.Errorf("verify %s: %w", artifact.Filename, err) } } diff --git a/service/updates/index_scan.go b/service/updates/index_scan.go index aa61417e..6e6a6af2 100644 --- a/service/updates/index_scan.go +++ b/service/updates/index_scan.go @@ -3,7 +3,6 @@ package updates import ( "crypto/sha256" "encoding/hex" - "encoding/json" "errors" "fmt" "io/fs" @@ -95,7 +94,7 @@ settings: } // GenerateIndexFromDir generates a index from a given folder. -func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) { +func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) { //nolint:maintidx artifacts := make(map[string]Artifact) // Initialize. @@ -107,6 +106,13 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) if err != nil { return nil, fmt.Errorf("invalid index dir: %w", err) } + var indexVersion *semver.Version + if cfg.Version != "" { + indexVersion, err = semver.NewVersion(cfg.Version) + if err != nil { + return nil, fmt.Errorf("invalid index version: %w", err) + } + } err = filepath.WalkDir(sourceDir, func(fullpath string, d fs.DirEntry, err error) error { // Fail on access error. @@ -227,9 +233,10 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) // Create base index. index := &Index{ - Name: cfg.Name, - Version: cfg.Version, - Published: time.Now(), + Name: cfg.Name, + Version: cfg.Version, + Published: time.Now(), + versionNum: indexVersion, } if index.Version == "" && cfg.PrimaryArtifact != "" { pv, ok := artifacts[cfg.PrimaryArtifact] @@ -286,45 +293,6 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) return index, nil } -func selectLatestArtifacts(artifacts []Artifact) ([]Artifact, error) { - artifactsMap := make(map[string]Artifact) - - for _, a := range artifacts { - // Make the key platform specific since there can be same filename for multiple platforms. - key := a.Filename + a.Platform - aMap, ok := artifactsMap[key] - if !ok { - artifactsMap[key] = a - continue - } - - if aMap.Version == "" || a.Version == "" { - return nil, fmt.Errorf("invalid mix version and non versioned files for: %s", a.Filename) - } - - mapVersion, err := semver.NewVersion(aMap.Version) - if err != nil { - return nil, fmt.Errorf("invalid version for artifact: %s", aMap.Filename) - } - - artifactVersion, err := semver.NewVersion(a.Version) - if err != nil { - return nil, fmt.Errorf("invalid version for artifact: %s", a.Filename) - } - - if mapVersion.LessThan(artifactVersion) { - artifactsMap[key] = a - } - } - - artifactsFiltered := make([]Artifact, 0, len(artifactsMap)) - for _, a := range artifactsMap { - artifactsFiltered = append(artifactsFiltered, a) - } - - return artifactsFiltered, nil -} - func getSHA256(path string, unpackType string) (string, error) { content, err := os.ReadFile(path) if err != nil { @@ -372,50 +340,3 @@ func getIdentifierAndVersion(versionedPath string) (identifier, version string, // `dirPath + filename` is guaranteed by path.Split() return dirPath + filename, version, true } - -// GenerateMockFolder generates mock index folder for testing. -func GenerateMockFolder(dir, name, version string) error { // FIXME: move this to test? - // Make sure dir exists - _ = os.MkdirAll(dir, defaultDirMode) - - // Create empty files - file, err := os.Create(filepath.Join(dir, "portmaster")) - if err != nil { - return err - } - _ = file.Close() - file, err = os.Create(filepath.Join(dir, "portmaster-core")) - if err != nil { - return err - } - _ = file.Close() - file, err = os.Create(filepath.Join(dir, "portmaster.zip")) - if err != nil { - return err - } - _ = file.Close() - file, err = os.Create(filepath.Join(dir, "assets.zip")) - if err != nil { - return err - } - _ = file.Close() - - index, err := GenerateIndexFromDir(dir, IndexScanConfig{ - Name: name, - Version: version, - }) - if err != nil { - return err - } - - indexJson, err := json.MarshalIndent(index, "", " ") - if err != nil { - fmt.Fprintf(os.Stderr, "failed to marshal index: %s\n", err) - } - - err = os.WriteFile(filepath.Join(dir, "index.json"), indexJson, defaultFileMode) - if err != nil { - return err - } - return nil -} diff --git a/service/updates/module.go b/service/updates/module.go index 8b04d3db..4ae6022c 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -10,11 +10,12 @@ import ( "sync" "time" + "github.com/tevino/abool" + "github.com/safing/jess" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" - "github.com/tevino/abool" ) const ( @@ -169,7 +170,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { // Fall back to scanning the directory. if !errors.Is(err, os.ErrNotExist) { - log.Errorf("updates/%s: invalid index file, falling back to dir scan: %w", cfg.Name, err) + log.Errorf("updates/%s: invalid index file, falling back to dir scan: %s", cfg.Name, err) } index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"}) if err == nil && index.init() == nil { @@ -181,13 +182,12 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { return module, nil } -func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreVersion, forceApply bool) (err error) { +func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreVersion, forceApply bool) (err error) { //nolint:maintidx // Make sure only one update process is running. if !u.isUpdateRunning.SetToIf(false, true) { return fmt.Errorf("an updater task is already running, please try again later") } defer u.isUpdateRunning.UnSet() - // FIXME: Switch to mutex? // Create a new downloader. downloader := NewDownloader(u, indexURLs) @@ -201,7 +201,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV } } else { // Otherwise, load index from download dir. - downloader.index, err = LoadIndex(filepath.Join(u.cfg.Directory, u.cfg.IndexFile), u.cfg.Verify) + downloader.index, err = LoadIndex(filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile), u.cfg.Verify) if err != nil { return fmt.Errorf("load previously downloaded index file: %w", err) } @@ -215,23 +215,42 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV u.indexLock.Unlock() // Check with local pointer to index. if err := index.ShouldUpgradeTo(downloader.index); err != nil { - log.Infof("updates/%s: no new or eligible update: %s", u.cfg.Name, err) - if u.cfg.Notify && u.instance.Notifications() != nil { - u.instance.Notifications().Notify(¬ifications.Notification{ - EventID: noNewUpdateNotificationID, - Type: notifications.Info, - Title: "Portmaster Is Up-To-Date", - Message: "Portmaster v" + index.Version + " is the newest version.", - Expires: time.Now().Add(1 * time.Minute).Unix(), - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "OK", + if errors.Is(err, ErrSameIndex) { + log.Infof("updates/%s: no new update", u.cfg.Name) + if u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: noNewUpdateNotificationID, + Type: notifications.Info, + Title: "Portmaster Is Up-To-Date", + Message: "Portmaster v" + index.Version + " is the newest version.", + Expires: time.Now().Add(1 * time.Minute).Unix(), + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "OK", + }, }, - }, - }) + }) + } + } else { + log.Warningf("updates/%s: cannot update: %s", u.cfg.Name, err) + if u.cfg.Notify && u.instance.Notifications() != nil { + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: noNewUpdateNotificationID, + Type: notifications.Info, + Title: "Portmaster Is Up-To-Date*", + Message: "While Portmaster v" + index.Version + " is the newest version, there is an internal issue with checking for updates: " + err.Error(), + Expires: time.Now().Add(1 * time.Minute).Unix(), + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "OK", + }, + }, + }) + } } - return ErrNoUpdateAvailable + return fmt.Errorf("%w: %w", ErrNoUpdateAvailable, err) } } @@ -320,7 +339,10 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV // Install is complete! // Clean up and notify modules of changed files. - u.cleanupAfterUpgrade() + err = u.cleanupAfterUpgrade() + if err != nil { + log.Debugf("updates/%s: failed to clean up after upgrade: %s", u.cfg.Name, err) + } u.EventResourcesUpdated.Submit(struct{}{}) // If no restart is needed, we are done. @@ -363,7 +385,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV Type: notifications.ActionTypeWebhook, Payload: notifications.ActionTypeWebhookPayload{ Method: "POST", - URL: "updates/apply", // FIXME + URL: "core/restart", }, }, ) @@ -376,15 +398,35 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV } func (u *Updater) updateCheckWorker(w *mgr.WorkerCtx) error { - _ = u.updateAndUpgrade(w, u.cfg.IndexURLs, false, false) - // FIXME: Handle errors. - return nil + err := u.updateAndUpgrade(w, u.cfg.IndexURLs, false, false) + switch { + case err == nil: + return nil // Success! + case errors.Is(err, ErrSameIndex): + return nil // Nothing to do. + case errors.Is(err, ErrNoUpdateAvailable): + return nil // Already logged. + case errors.Is(err, ErrActionRequired) && !u.cfg.Notify: + return fmt.Errorf("user action required, but notifying user is disabled: %w", err) + default: + return fmt.Errorf("udpating failed: %w", err) + } } func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error { - _ = u.updateAndUpgrade(w, u.cfg.IndexURLs, false, true) - // FIXME: Handle errors. - return nil + err := u.updateAndUpgrade(w, u.cfg.IndexURLs, false, true) + switch { + case err == nil: + return nil // Success! + case errors.Is(err, ErrSameIndex): + return nil // Nothing to do. + case errors.Is(err, ErrNoUpdateAvailable): + return nil // Already logged. + case errors.Is(err, ErrActionRequired) && !u.cfg.Notify: + return fmt.Errorf("user action required, but notifying user is disabled: %w", err) + default: + return fmt.Errorf("udpating failed: %w", err) + } } // ForceUpdate executes a forced update and upgrade directly and synchronously diff --git a/service/updates/updates_test.go b/service/updates/updates_test.go index 7ca86445..d79706c7 100644 --- a/service/updates/updates_test.go +++ b/service/updates/updates_test.go @@ -1,12 +1,15 @@ package updates import ( + "encoding/json" "fmt" "os" "path/filepath" "testing" + "time" "github.com/safing/portmaster/base/notifications" + "github.com/safing/portmaster/service/mgr" ) type testInstance struct{} @@ -24,66 +27,129 @@ func (i *testInstance) Ready() bool { func (i *testInstance) SetCmdLineOperation(f func() error) {} -func TestPreformUpdate(t *testing.T) { +func TestPerformUpdate(t *testing.T) { t.Parallel() // Initialize mock instance stub := &testInstance{} // Make tmp dirs - installedDir, err := os.MkdirTemp("", "updates_current") + installedDir, err := os.MkdirTemp("", "updates_current_") if err != nil { - panic(err) + t.Fatal(err) } defer func() { _ = os.RemoveAll(installedDir) }() - updateDir, err := os.MkdirTemp("", "updates_new") + updateDir, err := os.MkdirTemp("", "updates_new_") if err != nil { - panic(err) + t.Fatal(err) } defer func() { _ = os.RemoveAll(updateDir) }() - purgeDir, err := os.MkdirTemp("", "updates_purge") + purgeDir, err := os.MkdirTemp("", "updates_purge_") if err != nil { - panic(err) + t.Fatal(err) } defer func() { _ = os.RemoveAll(purgeDir) }() // Generate mock files - if err := GenerateMockFolder(installedDir, "Test", "1.0.0"); err != nil { - panic(err) + now := time.Now() + if err := GenerateMockFolder(installedDir, "Test", "1.0.0", now); err != nil { + t.Fatal(err) } - if err := GenerateMockFolder(updateDir, "Test", "1.0.1"); err != nil { - panic(err) + if err := GenerateMockFolder(updateDir, "Test", "1.0.1", now.Add(1*time.Minute)); err != nil { + t.Fatal(err) } - // Create updater - updates, err := New(stub, "Test", Config{ + // Create updater (loads index). + updater, err := New(stub, "Test", Config{ + Name: "Test", Directory: installedDir, DownloadDirectory: updateDir, PurgeDirectory: purgeDir, IndexFile: "index.json", - AutoApply: false, - NeedsRestart: false, + AutoDownload: true, + AutoApply: true, }) if err != nil { - panic(err) - } - // Read and parse the index file - if err := updates.downloader.Verify(); err != nil { - panic(err) + t.Fatal(err) } + // Try to apply the updates - err = updates.applyUpdates(nil) - if err != nil { - panic(err) - } + m := mgr.New("updates test") + _ = m.Do("test update and upgrade", func(w *mgr.WorkerCtx) error { + if err := updater.updateAndUpgrade(w, nil, false, false); err != nil { + if data, err := os.ReadFile(filepath.Join(installedDir, "index.json")); err == nil { + fmt.Println(string(data)) + fmt.Println(updater.index.Version) + fmt.Println(updater.index.versionNum) + } + if data, err := os.ReadFile(filepath.Join(updateDir, "index.json")); err == nil { + fmt.Println(string(data)) + idx, err := ParseIndex(data, nil) + if err == nil { + fmt.Println(idx.Version) + fmt.Println(idx.versionNum) + } + } - // CHeck if the current version is now the new. - bundle, err := LoadBundle(filepath.Join(installedDir, "index.json")) - if err != nil { - panic(err) - } + t.Fatal(err) + } + return nil + }) - if bundle.Version != "1.0.1" { - panic(fmt.Errorf("expected version 1.0.1 found %s", bundle.Version)) + // Check if the current version is now the new. + newIndex, err := LoadIndex(filepath.Join(installedDir, "index.json"), nil) + if err != nil { + t.Fatal(err) + } + if newIndex.Version != "1.0.1" { + t.Fatalf("expected version 1.0.1 found %s", newIndex.Version) } } + +// GenerateMockFolder generates mock index folder for testing. +func GenerateMockFolder(dir, name, version string, published time.Time) error { + // Make sure dir exists + _ = os.MkdirAll(dir, defaultDirMode) + + // Create empty files + file, err := os.Create(filepath.Join(dir, "portmaster")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "portmaster-core")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "portmaster.zip")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "assets.zip")) + if err != nil { + return err + } + _ = file.Close() + + index, err := GenerateIndexFromDir(dir, IndexScanConfig{ + Name: name, + Version: version, + }) + if err != nil { + return err + } + index.Published = published + + indexJSON, err := json.MarshalIndent(index, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal index: %s\n", err) + } + + err = os.WriteFile(filepath.Join(dir, "index.json"), indexJSON, defaultFileMode) + if err != nil { + return err + } + return nil +} diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go index ddb86bfa..593c3dd5 100644 --- a/service/updates/upgrade.go +++ b/service/updates/upgrade.go @@ -1,6 +1,7 @@ package updates import ( + "errors" "fmt" "io/fs" "os" @@ -31,7 +32,7 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error { } // Execute the upgrade. - upgradeError := u.upgradeMoveFiles(downloader, ignoreVersion) + upgradeError := u.upgradeMoveFiles(downloader) if upgradeError == nil { return nil } @@ -43,10 +44,10 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error { } // Recovery failed too. - return fmt.Errorf("upgrade (including recovery) failed: %s", u.cfg.Name, upgradeError) + return fmt.Errorf("upgrade (including recovery) failed: %w", upgradeError) } -func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) error { +func (u *Updater) upgradeMoveFiles(downloader *Downloader) error { // Important: // We assume that the downloader has done its job and all artifacts are verified. // Files will just be moved here. @@ -65,20 +66,28 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader, ignoreVersion bool) e } files, err := os.ReadDir(u.cfg.Directory) if err != nil { - return fmt.Errorf("read current directory: %w", err) - } - for _, file := range files { - // Check if file is ignored. - if slices.Contains(u.cfg.Ignore, file.Name()) { - continue + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("read current directory: %w", err) } - - // Otherwise, move file to purge dir. - src := filepath.Join(u.cfg.Directory, file.Name()) - dst := filepath.Join(u.cfg.PurgeDirectory, file.Name()) - err := u.moveFile(src, dst, "", file.Type().Perm()) + err = os.MkdirAll(u.cfg.Directory, defaultDirMode) if err != nil { - return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err) + return fmt.Errorf("create current directory: %w", err) + } + } else { + // Move files. + for _, file := range files { + // Check if file is ignored. + if slices.Contains(u.cfg.Ignore, file.Name()) { + continue + } + + // Otherwise, move file to purge dir. + src := filepath.Join(u.cfg.Directory, file.Name()) + dst := filepath.Join(u.cfg.PurgeDirectory, file.Name()) + err := u.moveFile(src, dst, "", file.Type().Perm()) + if err != nil { + return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err) + } } } @@ -118,7 +127,7 @@ func (u *Updater) moveFile(currentPath, newPath string, sha256sum string, fileMo // Moving was successful, return. return nil } - log.Tracef("updates/%s: failed to move to %q, falling back to copy+delete: %w", u.cfg.Name, newPath, err) + log.Tracef("updates/%s: failed to move to %q, falling back to copy+delete: %s", u.cfg.Name, newPath, err) // Copy and check the checksum while we are at it. err = copyAndCheckSHA256Sum(currentPath, newPath, sha256sum, fileMode) @@ -144,7 +153,7 @@ func (u *Updater) recoverFromFailedUpgrade() error { err := u.moveFile(purgedFile, activeFile, "", file.Type().Perm()) if err != nil { // Only warn and continue to recover as many files as possible. - log.Warningf("updates/%s: failed to roll back file %s: %w", u.cfg.Name, file.Name(), err) + log.Warningf("updates/%s: failed to roll back file %s: %s", u.cfg.Name, file.Name(), err) } } diff --git a/spn/instance.go b/spn/instance.go index 5c9b3957..ce555d15 100644 --- a/spn/instance.go +++ b/spn/instance.go @@ -34,10 +34,17 @@ import ( // Instance is an instance of a Portmaster service. type Instance struct { - ctx context.Context - cancelCtx context.CancelFunc + ctx context.Context + cancelCtx context.CancelFunc + + shutdownCtx context.Context + cancelShutdownCtx context.CancelFunc + serviceGroup *mgr.Group + binDir string + dataDir string + exitCode atomic.Int32 base *base.Base @@ -67,6 +74,7 @@ type Instance struct { terminal *terminal.TerminalModule CommandLineOperation func() error + ShouldRestart bool } // New returns a new Portmaster service instance. @@ -74,6 +82,7 @@ func New() (*Instance, error) { // Create instance to pass it to modules. instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) + instance.shutdownCtx, instance.cancelShutdownCtx = context.WithCancel(context.Background()) binaryUpdateIndex := updates.Config{ // FIXME: fill @@ -234,6 +243,18 @@ func (i *Instance) SetSleep(enabled bool) { } } +// BinDir returns the directory for binaries. +// This directory may be read-only. +func (i *Instance) BinDir() string { + return i.binDir +} + +// DataDir returns the directory for variable data. +// This directory is expected to be read/writeable. +func (i *Instance) DataDir() string { + return i.dataDir +} + // Database returns the database module. func (i *Instance) Database() *dbmodule.DBModule { return i.database @@ -379,12 +400,6 @@ func (i *Instance) Ready() bool { return i.serviceGroup.Ready() } -// Ctx returns the instance context. -// It is only canceled on shutdown. -func (i *Instance) Ctx() context.Context { - return i.ctx -} - // Start starts the instance. func (i *Instance) Start() error { return i.serviceGroup.Start() @@ -392,7 +407,6 @@ func (i *Instance) Start() error { // Stop stops the instance and cancels the instance context when done. func (i *Instance) Stop() error { - defer i.cancelCtx() return i.serviceGroup.Stop() } @@ -406,6 +420,8 @@ func (i *Instance) Restart() { i.core.EventRestart.Submit(struct{}{}) time.Sleep(10 * time.Millisecond) + // Set the restart flag and shutdown. + i.ShouldRestart = true i.shutdown(RestartExitCode) } @@ -419,32 +435,63 @@ func (i *Instance) Shutdown() { } func (i *Instance) shutdown(exitCode int) { + // Only shutdown once. + if i.IsShuttingDown() { + return + } + + // Cancel main context. + i.cancelCtx() + // Set given exit code. i.exitCode.Store(int32(exitCode)) + // Start shutdown asynchronously in a separate manager. m := mgr.New("instance") m.Go("shutdown", func(w *mgr.WorkerCtx) error { - for { - if err := i.Stop(); err != nil { - w.Error("failed to shutdown", "err", err, "retry", "1s") - time.Sleep(1 * time.Second) - } else { - return nil - } + // Stop all modules. + if err := i.Stop(); err != nil { + w.Error("failed to shutdown", "err", err) } + + // Cancel shutdown process context. + i.cancelShutdownCtx() + return nil }) } -// Stopping returns whether the instance is shutting down. -func (i *Instance) Stopping() bool { - return i.ctx.Err() == nil +// Ctx returns the instance context. +// It is canceled when shutdown is started. +func (i *Instance) Ctx() context.Context { + return i.ctx } -// Stopped returns a channel that is triggered when the instance has shut down. -func (i *Instance) Stopped() <-chan struct{} { +// IsShuttingDown returns whether the instance is shutting down. +func (i *Instance) IsShuttingDown() bool { + return i.ctx.Err() != nil +} + +// ShuttingDown returns a channel that is triggered when the instance starts shutting down. +func (i *Instance) ShuttingDown() <-chan struct{} { return i.ctx.Done() } +// ShutdownCtx returns the instance shutdown context. +// It is canceled when shutdown is complete. +func (i *Instance) ShutdownCtx() context.Context { + return i.shutdownCtx +} + +// IsShutDown returns whether the instance has stopped. +func (i *Instance) IsShutDown() bool { + return i.shutdownCtx.Err() != nil +} + +// ShutDownComplete returns a channel that is triggered when the instance has shut down. +func (i *Instance) ShutdownComplete() <-chan struct{} { + return i.shutdownCtx.Done() +} + // ExitCode returns the set exit code of the instance. func (i *Instance) ExitCode() int { return int(i.exitCode.Load()) From 8b1bdc7eb1bc5e9161761efd6d0f48935f4d1ade Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 12 Nov 2024 13:30:47 +0100 Subject: [PATCH 61/62] WIP --- base/info/version.go | 2 + cmds/hub/main.go | 5 ++- cmds/observation-hub/main.go | 4 +- cmds/portmaster-core/main.go | 2 +- service/config.go | 3 +- service/process/process.go | 2 +- service/updates.go | 16 ++++++- service/updates/module.go | 82 +++++++++++++++++------------------- service/updates/upgrade.go | 4 +- 9 files changed, 70 insertions(+), 50 deletions(-) diff --git a/base/info/version.go b/base/info/version.go index 91bad092..6140adc2 100644 --- a/base/info/version.go +++ b/base/info/version.go @@ -10,6 +10,8 @@ import ( "sync" ) +// FIXME: version does not show in portmaster + var ( name string license string diff --git a/cmds/hub/main.go b/cmds/hub/main.go index b0b6e1d1..9c0379e8 100644 --- a/cmds/hub/main.go +++ b/cmds/hub/main.go @@ -24,6 +24,7 @@ import ( func init() { // flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") + // FIXME } var sigUSR1 = syscall.Signal(0xa) @@ -48,6 +49,8 @@ func main() { log.SetLogLevel(log.WarningLevel) _ = log.Start() + // FIXME: Use service? + // Create instance. var execCmdLine bool instance, err := spn.New() @@ -109,7 +112,7 @@ func main() { slog.Warn("program was interrupted, stopping") } - case <-instance.Stopped(): + case <-instance.ShutdownComplete(): log.Shutdown() os.Exit(instance.ExitCode()) } diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go index 1cb9bafd..a6ced810 100644 --- a/cmds/observation-hub/main.go +++ b/cmds/observation-hub/main.go @@ -78,6 +78,8 @@ func main() { } instance.AddModule(observer) + // FIXME: Use service? + // Execute command line operation, if requested or available. switch { case !execCmdLine: @@ -126,7 +128,7 @@ func main() { slog.Warn("program was interrupted, stopping") } - case <-instance.Stopped(): + case <-instance.ShuttingDown(): log.Shutdown() os.Exit(instance.ExitCode()) } diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 993ffb55..0b4d0e01 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -58,7 +58,7 @@ func initializeGlobals(cmd *cobra.Command, args []string) { svcCfg = &service.ServiceConfig{ BinDir: binDir, DataDir: dataDir, - BinariesIndexURLs: service.DefaultBinaryIndexURLs, + BinariesIndexURLs: service.DefaultStableBinaryIndexURLs, IntelIndexURLs: service.DefaultIntelIndexURLs, VerifyBinaryUpdates: service.BinarySigningTrustStore, VerifyIntelUpdates: service.BinarySigningTrustStore, diff --git a/service/config.go b/service/config.go index b9d9b5c1..0d613315 100644 --- a/service/config.go +++ b/service/config.go @@ -60,7 +60,8 @@ func (sc *ServiceConfig) Init() error { // Apply defaults for required fields. if len(sc.BinariesIndexURLs) == 0 { - sc.BinariesIndexURLs = DefaultBinaryIndexURLs + // FIXME: Select based on setting. + sc.BinariesIndexURLs = DefaultStableBinaryIndexURLs } if len(sc.IntelIndexURLs) == 0 { sc.IntelIndexURLs = DefaultIntelIndexURLs diff --git a/service/process/process.go b/service/process/process.go index 60dac7eb..4e0eeeae 100644 --- a/service/process/process.go +++ b/service/process/process.go @@ -256,7 +256,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (* // Username process.UserName, err = pInfo.UsernameWithContext(ctx) if err != nil { - return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pInfo.Pid, err) + log.Tracer(ctx).Warningf("process: failed to get username (PID %d): %s", pInfo.Pid, err) } // TODO: User Home diff --git a/service/updates.go b/service/updates.go index c9ee185e..3c717951 100644 --- a/service/updates.go +++ b/service/updates.go @@ -9,9 +9,19 @@ import ( ) var ( - DefaultBinaryIndexURLs = []string{ + DefaultStableBinaryIndexURLs = []string{ "https://updates.safing.io/stable.v3.json", } + DefaultBetaBinaryIndexURLs = []string{ + "https://updates.safing.io/beta.v3.json", + } + DefaultStagingBinaryIndexURLs = []string{ + "https://updates.safing.io/staging.v3.json", + } + DefaultSupportBinaryIndexURLs = []string{ + "https://updates.safing.io/support.v3.json", + } + DefaultIntelIndexURLs = []string{ "https://updates.safing.io/intel.v3.json", } @@ -53,6 +63,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo IndexURLs: svcCfg.BinariesIndexURLs, IndexFile: "index.json", Verify: svcCfg.VerifyBinaryUpdates, + AutoCheck: true, // FIXME: Get from setting. AutoDownload: false, AutoApply: false, NeedsRestart: true, @@ -66,6 +77,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo IndexURLs: svcCfg.IntelIndexURLs, IndexFile: "index.json", Verify: svcCfg.VerifyIntelUpdates, + AutoCheck: true, // FIXME: Get from setting. AutoDownload: true, AutoApply: true, NeedsRestart: false, @@ -82,6 +94,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo IndexURLs: svcCfg.BinariesIndexURLs, IndexFile: "index.json", Verify: svcCfg.VerifyBinaryUpdates, + AutoCheck: true, // FIXME: Get from setting. AutoDownload: false, AutoApply: false, NeedsRestart: true, @@ -95,6 +108,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo IndexURLs: svcCfg.IntelIndexURLs, IndexFile: "index.json", Verify: svcCfg.VerifyIntelUpdates, + AutoCheck: true, // FIXME: Get from setting. AutoDownload: true, AutoApply: true, NeedsRestart: false, diff --git a/service/updates/module.go b/service/updates/module.go index 4ae6022c..e5b32129 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -41,6 +41,7 @@ var ( ErrNotFound = errors.New("file not found") ErrSameIndex = errors.New("same index") + ErrAutoCheckDisabled = errors.New("automatic update checks are disabled") ErrNoUpdateAvailable = errors.New("no update available") ErrActionRequired = errors.New("action required") ) @@ -67,6 +68,8 @@ type Config struct { // Verify enables and specifies the trust the index signatures will be checked against. Verify jess.TrustStore + // AutoCheck defines that new indexes may be downloaded automatically without outside trigger. + AutoCheck bool // AutoDownload defines that updates may be downloaded automatically without outside trigger. AutoDownload bool // AutoApply defines that updates may be automatically applied without outside trigger. @@ -157,8 +160,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { } // Create Workers. - module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.updateCheckWorker, nil). - Repeat(updateTaskRepeatDuration) + module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.updateCheckWorker, nil) module.upgradeWorkerMgr = m.NewWorkerMgr("upgrader", module.upgradeWorker, nil) // Load index. @@ -207,12 +209,13 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV } } + // Get index to check version. + u.indexLock.Lock() + index := u.index + u.indexLock.Unlock() + // Check if there is a new version. - if !ignoreVersion { - // Get index to check version. - u.indexLock.Lock() - index := u.index - u.indexLock.Unlock() + if !ignoreVersion && index != nil { // Check with local pointer to index. if err := index.ShouldUpgradeTo(downloader.index); err != nil { if errors.Is(err, ErrSameIndex) { @@ -351,44 +354,31 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV } // Notify user that a restart is required. - if u.cfg.Notify && u.instance.Notifications() != nil { - - u.instance.Notifications().Notify(¬ifications.Notification{ - EventID: restartRequiredNotificationID, - Type: notifications.Info, - Title: "Restart Required", - Message: "Portmaster v" + downloader.index.Version + " is installed. Restart to use new version.", - AvailableActions: []*notifications.Action{ - { - ID: "ack", - Text: "Later", - }, - { - ID: "restart", - Text: "Restart Now", - Type: notifications.ActionTypeWebhook, - Payload: notifications.ActionTypeWebhookPayload{ - Method: "POST", - URL: "updates/apply", + if u.cfg.Notify { + if u.instance.Notifications() != nil { + u.instance.Notifications().Notify(¬ifications.Notification{ + EventID: restartRequiredNotificationID, + Type: notifications.Info, + Title: "Restart Required", + Message: "Portmaster v" + downloader.index.Version + " is installed. Restart to use new version.", + AvailableActions: []*notifications.Action{ + { + ID: "ack", + Text: "Later", + }, + { + ID: "restart", + Text: "Restart Now", + Type: notifications.ActionTypeWebhook, + Payload: notifications.ActionTypeWebhookPayload{ + Method: "POST", + URL: "updates/apply", + }, }, }, - }, - }) + }) + } - u.instance.Notifications().NotifyInfo( - updateAvailableNotificationID, - "Restart Required", - "Portmaster v"+downloader.index.Version+" is installed. Restart to use new version.", - notifications.Action{ - ID: "restart", - Text: "Restart Now", - Type: notifications.ActionTypeWebhook, - Payload: notifications.ActionTypeWebhookPayload{ - Method: "POST", - URL: "core/restart", - }, - }, - ) return fmt.Errorf("%w: restart required", ErrActionRequired) } @@ -470,6 +460,7 @@ func (u *Updater) Manager() *mgr.Manager { // Start starts the module. func (u *Updater) Start() error { if u.corruptedInstallation && u.cfg.Notify && u.instance.Notifications() != nil { + // FIXME: this might make sense as a module state u.instance.Notifications().NotifyError( corruptInstallationNotificationID, "Install Corruption", @@ -477,7 +468,12 @@ func (u *Updater) Start() error { ) } - u.updateCheckWorkerMgr.Delay(15 * time.Second) + // Check for updates automatically, if enabled. + if u.cfg.AutoCheck { + u.updateCheckWorkerMgr. + Repeat(updateTaskRepeatDuration). + Delay(15 * time.Second) + } return nil } diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go index 593c3dd5..86077ee1 100644 --- a/service/updates/upgrade.go +++ b/service/updates/upgrade.go @@ -12,6 +12,8 @@ import ( "github.com/safing/portmaster/base/log" ) +// FIXME: previous update system did in-place service file upgrades. Check if this is still necessary and if changes are in current installers. + const ( defaultFileMode = os.FileMode(0o0644) executableFileMode = os.FileMode(0o0744) @@ -25,7 +27,7 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error { defer u.indexLock.Unlock() // Check if we should upgrade at all. - if !ignoreVersion { + if !ignoreVersion && u.index != nil { if err := u.index.ShouldUpgradeTo(downloader.index); err != nil { return fmt.Errorf("cannot upgrade: %w", ErrNoUpdateAvailable) } From f91003d077d809810bff1b2545465b34d9808882 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 14 Nov 2024 17:33:27 +0100 Subject: [PATCH 62/62] [service] Move logging to the core, remove pkg level logs --- base/api/endpoints_debug.go | 5 +- base/info/version.go | 22 ++++ base/log/flags.go | 13 --- base/log/input.go | 39 ++----- base/log/logging.go | 96 +++++++++-------- base/log/logging_test.go | 5 +- base/log/output.go | 162 ++++++++++++---------------- base/log/slog.go | 41 +++---- base/log/trace.go | 33 +----- base/log/writer.go | 119 ++++++++++++++++++++ cmds/hub/main.go | 5 +- cmds/observation-hub/main.go | 5 +- cmds/portmaster-core/main.go | 18 +++- cmds/portmaster-core/run.go | 16 ++- cmds/portmaster-core/run_linux.go | 7 +- cmds/portmaster-core/run_windows.go | 10 +- cmds/portmaster-core/update.go | 5 +- go.mod | 13 +-- go.sum | 18 ---- service/config.go | 23 +++- service/mgr/worker.go | 5 +- spn/instance.go | 6 ++ 22 files changed, 360 insertions(+), 306 deletions(-) delete mode 100644 base/log/flags.go create mode 100644 base/log/writer.go diff --git a/base/api/endpoints_debug.go b/base/api/endpoints_debug.go index 55865d9c..06de4cd5 100644 --- a/base/api/endpoints_debug.go +++ b/base/api/endpoints_debug.go @@ -12,6 +12,7 @@ import ( "time" "github.com/safing/portmaster/base/info" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/utils/debug" ) @@ -152,12 +153,12 @@ func getStack(_ *Request) (data []byte, err error) { // printStack prints the current goroutine stack to stderr. func printStack(_ *Request) (msg string, err error) { - _, err = fmt.Fprint(os.Stderr, "===== PRINTING STACK =====\n") + _, err = fmt.Fprint(log.GlobalWriter, "===== PRINTING STACK =====\n") if err == nil { err = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) } if err == nil { - _, err = fmt.Fprint(os.Stderr, "===== END OF STACK =====\n") + _, err = fmt.Fprint(log.GlobalWriter, "===== END OF STACK =====\n") } if err != nil { return "", err diff --git a/base/info/version.go b/base/info/version.go index 6140adc2..2c6c3058 100644 --- a/base/info/version.go +++ b/base/info/version.go @@ -153,6 +153,28 @@ func FullVersion() string { return builder.String() } +// CondensedVersion returns the rather complete, but condensed version string. +func CondensedVersion() string { + info := GetInfo() + + cgoInfo := "-cgo" + if info.CGO { + cgoInfo = "+cgo" + } + dirtyInfo := "clean" + if info.Dirty { + dirtyInfo = "dirty" + } + + return fmt.Sprintf( + "%s %s (%s; built with %s [%s %s] from %s [%s] at %s)", + info.Name, version, + runtime.GOOS, + runtime.Version(), runtime.Compiler, cgoInfo, + info.Commit, dirtyInfo, info.CommitTime, + ) +} + // CheckVersion checks if the metadata is ok. func CheckVersion() error { switch { diff --git a/base/log/flags.go b/base/log/flags.go deleted file mode 100644 index eb019297..00000000 --- a/base/log/flags.go +++ /dev/null @@ -1,13 +0,0 @@ -package log - -import "flag" - -var ( - logLevelFlag string - pkgLogLevelsFlag string -) - -func init() { - flag.StringVar(&logLevelFlag, "log", "", "set log level to [trace|debug|info|warning|error|critical]") - flag.StringVar(&pkgLogLevelsFlag, "plog", "", "set log level of packages: database=trace,notifications=debug") -} diff --git a/base/log/input.go b/base/log/input.go index ef8564a9..6dd4aa96 100644 --- a/base/log/input.go +++ b/base/log/input.go @@ -3,7 +3,6 @@ package log import ( "fmt" "runtime" - "strings" "sync/atomic" "time" ) @@ -25,6 +24,11 @@ func log(level Severity, msg string, tracer *ContextTracer) { return } + // Check log level. + if uint32(level) < atomic.LoadUint32(logLevel) { + return + } + // get time now := time.Now() @@ -41,31 +45,6 @@ func log(level Severity, msg string, tracer *ContextTracer) { } } - // check if level is enabled for file or generally - if pkgLevelsActive.IsSet() { - pathSegments := strings.Split(file, "/") - if len(pathSegments) < 2 { - // file too short for package levels - return - } - pkgLevelsLock.Lock() - severity, ok := pkgLevels[pathSegments[len(pathSegments)-2]] - pkgLevelsLock.Unlock() - if ok { - if level < severity { - return - } - } else { - // no package level set, check against global level - if uint32(level) < atomic.LoadUint32(logLevel) { - return - } - } - } else if uint32(level) < atomic.LoadUint32(logLevel) { - // no package levels set, check against global level - return - } - // create log object log := &logLine{ msg: msg, @@ -101,13 +80,7 @@ func log(level Severity, msg string, tracer *ContextTracer) { } func fastcheck(level Severity) bool { - if pkgLevelsActive.IsSet() { - return true - } - if uint32(level) >= atomic.LoadUint32(logLevel) { - return true - } - return false + return uint32(level) >= atomic.LoadUint32(logLevel) } // Trace is used to log tiny steps. Log traces to context if you can! diff --git a/base/log/logging.go b/base/log/logging.go index b859bf11..d9ed43d9 100644 --- a/base/log/logging.go +++ b/base/log/logging.go @@ -2,6 +2,7 @@ package log import ( "fmt" + "log/slog" "os" "strings" "sync" @@ -33,6 +34,26 @@ import ( // Severity describes a log level. type Severity uint32 +func (s Severity) toSLogLevel() slog.Level { + // Convert to slog level. + switch s { + case TraceLevel: + return slog.LevelDebug + case DebugLevel: + return slog.LevelDebug + case InfoLevel: + return slog.LevelInfo + case WarningLevel: + return slog.LevelWarn + case ErrorLevel: + return slog.LevelError + case CriticalLevel: + return slog.LevelError + } + // Failed to convert, return default log level + return slog.LevelWarn +} + // Message describes a log level message and is implemented // by logLine. type Message interface { @@ -105,10 +126,6 @@ var ( logLevelInt = uint32(InfoLevel) logLevel = &logLevelInt - pkgLevelsActive = abool.NewBool(false) - pkgLevels = make(map[string]Severity) - pkgLevelsLock sync.Mutex - logsWaiting = make(chan struct{}, 1) logsWaitingFlag = abool.NewBool(false) @@ -121,19 +138,6 @@ var ( startedSignal = make(chan struct{}) ) -// SetPkgLevels sets individual log levels for packages. Only effective after Start(). -func SetPkgLevels(levels map[string]Severity) { - pkgLevelsLock.Lock() - pkgLevels = levels - pkgLevelsLock.Unlock() - pkgLevelsActive.Set() -} - -// UnSetPkgLevels removes all individual log levels for packages. -func UnSetPkgLevels() { - pkgLevelsActive.UnSet() -} - // GetLogLevel returns the current log level. func GetLogLevel() Severity { return Severity(atomic.LoadUint32(logLevel)) @@ -187,47 +191,36 @@ func ParseLevel(level string) Severity { } // Start starts the logging system. Must be called in order to see logs. -func Start() (err error) { +func Start(level string, logToStdout bool, logDir string) (err error) { if !initializing.SetToIf(false, true) { return nil } - logBuffer = make(chan *logLine, 1024) - - if logLevelFlag != "" { - initialLogLevel := ParseLevel(logLevelFlag) + // Parse log level argument. + initialLogLevel := InfoLevel + if level != "" { + initialLogLevel = ParseLevel(level) if initialLogLevel == 0 { - fmt.Fprintf(os.Stderr, "log warning: invalid log level \"%s\", falling back to level info\n", logLevelFlag) + fmt.Fprintf(os.Stderr, "log warning: invalid log level %q, falling back to level info\n", level) initialLogLevel = InfoLevel } + } - SetLogLevel(initialLogLevel) + // Setup writer. + if logToStdout { + GlobalWriter = NewStdoutWriter() } else { - // Setup slog here for the transition period. - setupSLog(GetLogLevel()) + // Create file log writer. + var err error + GlobalWriter, err = NewFileWriter(logDir) + if err != nil { + return fmt.Errorf("failed to initialize log file: %w", err) + } } - // get and set file loglevels - pkgLogLevels := pkgLogLevelsFlag - if len(pkgLogLevels) > 0 { - newPkgLevels := make(map[string]Severity) - for _, pair := range strings.Split(pkgLogLevels, ",") { - splitted := strings.Split(pair, "=") - if len(splitted) != 2 { - err = fmt.Errorf("log warning: invalid file log level \"%s\", ignoring", pair) - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - break - } - fileLevel := ParseLevel(splitted[1]) - if fileLevel == 0 { - err = fmt.Errorf("log warning: invalid file log level \"%s\", ignoring", pair) - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - break - } - newPkgLevels[splitted[0]] = fileLevel - } - SetPkgLevels(newPkgLevels) - } + // Init logging systems. + SetLogLevel(initialLogLevel) + logBuffer = make(chan *logLine, 1024) if !schedulingEnabled { close(writeTrigger) @@ -237,6 +230,14 @@ func Start() (err error) { started.Set() close(startedSignal) + // Delete all logs older than one month. + if !logToStdout { + err = CleanOldLogs(logDir, 30*24*time.Hour) + if err != nil { + Errorf("log: failed to clean old log files: %s", err) + } + } + return err } @@ -246,4 +247,5 @@ func Shutdown() { close(shutdownSignal) } shutdownWaitGroup.Wait() + GlobalWriter.Close() } diff --git a/base/log/logging_test.go b/base/log/logging_test.go index 577ee51a..6996389a 100644 --- a/base/log/logging_test.go +++ b/base/log/logging_test.go @@ -7,7 +7,7 @@ import ( ) func init() { - err := Start() + err := Start("info", true, "") if err != nil { panic(fmt.Sprintf("start failed: %s", err)) } @@ -56,9 +56,6 @@ func TestLogging(t *testing.T) { // wait logs to be written time.Sleep(1 * time.Millisecond) - // just for show - UnSetPkgLevels() - // do not really shut down, we may need logging for other tests // ShutdownLogging() } diff --git a/base/log/output.go b/base/log/output.go index a2947dc5..91acd525 100644 --- a/base/log/output.go +++ b/base/log/output.go @@ -2,78 +2,25 @@ package log import ( "fmt" - "os" "runtime/debug" "sync" "time" + + "github.com/safing/portmaster/base/info" ) -type ( - // Adapter is used to write logs. - Adapter interface { - // Write is called for each log message. - Write(msg Message, duplicates uint64) - } - - // AdapterFunc is a convenience type for implementing - // Adapter. - AdapterFunc func(msg Message, duplicates uint64) - - // FormatFunc formats msg into a string. - FormatFunc func(msg Message, duplicates uint64) string - - // SimpleFileAdapter implements Adapter and writes all - // messages to File. - SimpleFileAdapter struct { - Format FormatFunc - File *os.File - } -) +// Adapter is used to write logs. +type Adapter interface { + // Write is called for each log message. + WriteMessage(msg Message, duplicates uint64) +} var ( - // StdoutAdapter is a simple file adapter that writes - // all logs to os.Stdout using a predefined format. - StdoutAdapter = &SimpleFileAdapter{ - File: os.Stdout, - Format: defaultColorFormater, - } - - // StderrAdapter is a simple file adapter that writes - // all logs to os.Stdout using a predefined format. - StderrAdapter = &SimpleFileAdapter{ - File: os.Stderr, - Format: defaultColorFormater, - } -) - -var ( - adapter Adapter = StdoutAdapter - schedulingEnabled = false writeTrigger = make(chan struct{}) ) -// SetAdapter configures the logging adapter to use. -// This must be called before the log package is initialized. -func SetAdapter(a Adapter) { - if initializing.IsSet() || a == nil { - return - } - - adapter = a -} - -// Write implements Adapter and calls fn. -func (fn AdapterFunc) Write(msg Message, duplicates uint64) { - fn(msg, duplicates) -} - -// Write implements Adapter and writes msg the underlying file. -func (fileAdapter *SimpleFileAdapter) Write(msg Message, duplicates uint64) { - fmt.Fprintln(fileAdapter.File, fileAdapter.Format(msg, duplicates)) -} - -// EnableScheduling enables external scheduling of the logger. This will require to manually trigger writes via TriggerWrite whenevery logs should be written. Please note that full buffers will also trigger writing. Must be called before Start() to have an effect. +// EnableScheduling enables external scheduling of the logger. This will require to manually trigger writes via TriggerWrite whenever logs should be written. Please note that full buffers will also trigger writing. Must be called before Start() to have an effect. func EnableScheduling() { if !initializing.IsSet() { schedulingEnabled = true @@ -95,27 +42,47 @@ func TriggerWriterChannel() chan struct{} { return writeTrigger } -func defaultColorFormater(line Message, duplicates uint64) string { - return formatLine(line.(*logLine), duplicates, true) //nolint:forcetypeassert // TODO: improve -} - func startWriter() { - fmt.Printf( - "%s%s%s %sBOF %s%s\n", + if GlobalWriter.isStdout { + fmt.Fprintf(GlobalWriter, + "%s%s%s %sBOF %s%s\n", - dimColor(), - time.Now().Format(timeFormat), - endDimColor(), + dimColor(), + time.Now().Format(timeFormat), + endDimColor(), - blueColor(), - rightArrow, - endColor(), - ) + blueColor(), + rightArrow, + endColor(), + ) + } else { + fmt.Fprintf(GlobalWriter, + "%s BOF %s\n", + time.Now().Format(timeFormat), + rightArrow, + ) + } + writeVersion() shutdownWaitGroup.Add(1) go writerManager() } +func writeVersion() { + if GlobalWriter.isStdout { + fmt.Fprintf(GlobalWriter, "%s%s%s running %s%s%s\n", + dimColor(), + time.Now().Format(timeFormat), + endDimColor(), + + blueColor(), + info.CondensedVersion(), + endColor()) + } else { + fmt.Fprintf(GlobalWriter, "%s running %s\n", time.Now().Format(timeFormat), info.CondensedVersion()) + } +} + func writerManager() { defer shutdownWaitGroup.Done() @@ -129,18 +96,17 @@ func writerManager() { } } -// defer should be able to edit the err. So naked return is required. -// nolint:golint,nakedret -func writer() (err error) { +func writer() error { + var err error defer func() { // recover from panic panicVal := recover() if panicVal != nil { - err = fmt.Errorf("%s", panicVal) + _, err = fmt.Fprintf(GlobalWriter, "%s", panicVal) // write stack to stderr fmt.Fprintf( - os.Stderr, + GlobalWriter, `===== Error Report ===== Message: %s StackTrace: @@ -169,7 +135,7 @@ StackTrace: case <-forceEmptyingOfBuffer: // log buffer is full! case <-shutdownSignal: // shutting down finalizeWriting() - return + return err } // wait for timeslot to log @@ -178,7 +144,7 @@ StackTrace: case <-forceEmptyingOfBuffer: // log buffer is full! case <-shutdownSignal: // shutting down finalizeWriting() - return + return err } // write all the logs! @@ -201,7 +167,7 @@ StackTrace: } // if currentLine and line are _not_ equal, output currentLine - adapter.Write(currentLine, duplicates) + GlobalWriter.WriteMessage(currentLine, duplicates) // add to unexpected logs addUnexpectedLogs(currentLine) // reset duplicate counter @@ -215,7 +181,7 @@ StackTrace: // write final line if currentLine != nil { - adapter.Write(currentLine, duplicates) + GlobalWriter.WriteMessage(currentLine, duplicates) // add to unexpected logs addUnexpectedLogs(currentLine) } @@ -225,7 +191,7 @@ StackTrace: case <-time.After(10 * time.Millisecond): case <-shutdownSignal: finalizeWriting() - return + return err } } @@ -235,19 +201,27 @@ func finalizeWriting() { for { select { case line := <-logBuffer: - adapter.Write(line, 0) + GlobalWriter.WriteMessage(line, 0) case <-time.After(10 * time.Millisecond): - fmt.Printf( - "%s%s%s %sEOF %s%s\n", + if GlobalWriter.isStdout { + fmt.Fprintf(GlobalWriter, + "%s%s%s %sEOF %s%s\n", - dimColor(), - time.Now().Format(timeFormat), - endDimColor(), + dimColor(), + time.Now().Format(timeFormat), + endDimColor(), - blueColor(), - leftArrow, - endColor(), - ) + blueColor(), + leftArrow, + endColor(), + ) + } else { + fmt.Fprintf(GlobalWriter, + "%s EOF %s\n", + time.Now().Format(timeFormat), + leftArrow, + ) + } return } } diff --git a/base/log/slog.go b/base/log/slog.go index d0f09aad..5901c146 100644 --- a/base/log/slog.go +++ b/base/log/slog.go @@ -6,54 +6,36 @@ import ( "runtime" "github.com/lmittmann/tint" - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" ) -func setupSLog(logLevel Severity) { - // Convert to slog level. - var level slog.Level - switch logLevel { - case TraceLevel: - level = slog.LevelDebug - case DebugLevel: - level = slog.LevelDebug - case InfoLevel: - level = slog.LevelInfo - case WarningLevel: - level = slog.LevelWarn - case ErrorLevel: - level = slog.LevelError - case CriticalLevel: - level = slog.LevelError - } +func setupSLog(level Severity) { + // Set highest possible level, so it can be changed in runtime. + handlerLogLevel := level.toSLogLevel() - // Setup logging. - // Define output. - logOutput := os.Stdout // Create handler depending on OS. var logHandler slog.Handler switch runtime.GOOS { case "windows": logHandler = tint.NewHandler( - colorable.NewColorable(logOutput), + GlobalWriter, &tint.Options{ AddSource: true, - Level: level, + Level: handlerLogLevel, TimeFormat: timeFormat, + NoColor: !GlobalWriter.IsStdout(), // FIXME: also check for tty. }, ) case "linux": - logHandler = tint.NewHandler(logOutput, &tint.Options{ + logHandler = tint.NewHandler(GlobalWriter, &tint.Options{ AddSource: true, - Level: level, + Level: handlerLogLevel, TimeFormat: timeFormat, - NoColor: !isatty.IsTerminal(logOutput.Fd()), + NoColor: !GlobalWriter.IsStdout(), // FIXME: also check for tty. }) default: logHandler = tint.NewHandler(os.Stdout, &tint.Options{ AddSource: true, - Level: level, + Level: handlerLogLevel, TimeFormat: timeFormat, NoColor: true, }) @@ -61,5 +43,6 @@ func setupSLog(logLevel Severity) { // Set as default logger. slog.SetDefault(slog.New(logHandler)) - slog.SetLogLoggerLevel(level) + // Set actual log level. + slog.SetLogLoggerLevel(handlerLogLevel) } diff --git a/base/log/trace.go b/base/log/trace.go index 640594d4..2226339c 100644 --- a/base/log/trace.go +++ b/base/log/trace.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "runtime" - "strings" "sync" "sync/atomic" "time" @@ -24,36 +23,8 @@ var key = ContextTracerKey{} // AddTracer adds a ContextTracer to the returned Context. Will return a nil ContextTracer if logging level is not set to trace. Will return a nil ContextTracer if one already exists. Will return a nil ContextTracer in case of an error. Will return a nil context if nil. func AddTracer(ctx context.Context) (context.Context, *ContextTracer) { if ctx != nil && fastcheck(TraceLevel) { - // check pkg levels - if pkgLevelsActive.IsSet() { - // get file - _, file, _, ok := runtime.Caller(1) - if !ok { - // cannot get file, ignore - return ctx, nil - } - - pathSegments := strings.Split(file, "/") - if len(pathSegments) < 2 { - // file too short for package levels - return ctx, nil - } - pkgLevelsLock.Lock() - severity, ok := pkgLevels[pathSegments[len(pathSegments)-2]] - pkgLevelsLock.Unlock() - if ok { - // check against package level - if TraceLevel < severity { - return ctx, nil - } - } else { - // no package level set, check against global level - if uint32(TraceLevel) < atomic.LoadUint32(logLevel) { - return ctx, nil - } - } - } else if uint32(TraceLevel) < atomic.LoadUint32(logLevel) { - // no package levels set, check against global level + // Check log level. + if atomic.LoadUint32(logLevel) > uint32(TraceLevel) { return ctx, nil } diff --git a/base/log/writer.go b/base/log/writer.go new file mode 100644 index 00000000..6c10716f --- /dev/null +++ b/base/log/writer.go @@ -0,0 +1,119 @@ +package log + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +// GlobalWriter is the global log writer. +var GlobalWriter *LogWriter = nil + +type LogWriter struct { + writeLock sync.Mutex + isStdout bool + file *os.File +} + +// NewStdoutWriter creates a new log writer thet will write to the stdout. +func NewStdoutWriter() *LogWriter { + return &LogWriter{ + file: os.Stdout, + isStdout: true, + } +} + +// NewFileWriter creates a new log writer that will write to a file. The file path will be /2006-01-02_15-04-05.log (with current date and time) +func NewFileWriter(dir string) (*LogWriter, error) { + // Make sure log dir exists, if not, create with strict permission, as logs can contain sensitive data. + _ = os.MkdirAll(dir, 0o700) + + // Open new log file. + logFile := time.Now().UTC().Format("2006-01-02_15-04-05") + ".log" + file, err := os.Create(filepath.Join(dir, logFile)) + if err != nil { + return nil, err + } + + return &LogWriter{ + file: file, + isStdout: false, + }, nil +} + +// Write writes the buffer to the writer. +func (l *LogWriter) Write(buf []byte) (int, error) { + if l == nil { + return 0, fmt.Errorf("log writer not initialized") + } + + // No need to lock in stdout context. + if !l.isStdout { + l.writeLock.Lock() + defer l.writeLock.Unlock() + } + + return l.file.Write(buf) +} + +// WriteMessage writes the message to the writer. +func (l *LogWriter) WriteMessage(msg Message, duplicates uint64) { + if l == nil { + return + } + + // No need to lock in stdout context. + if !l.isStdout { + l.writeLock.Lock() + defer l.writeLock.Unlock() + } + + fmt.Fprintln(l.file, formatLine(msg.(*logLine), duplicates, l.isStdout)) +} + +// IsStdout returns true if writer was initialized with stdout. +func (l *LogWriter) IsStdout() bool { + return l != nil && l.isStdout +} + +// Close closes the writer. +func (l *LogWriter) Close() { + if l != nil && !l.isStdout { + _ = l.file.Close() + } +} + +// CleanOldLogs deletes all log files in given directory that are older than the given threshold. +func CleanOldLogs(dir string, threshold time.Duration) error { + // Get current log file name. + var currentLogFile string + if GlobalWriter != nil && GlobalWriter.file != nil { + currentLogFile = GlobalWriter.file.Name() + } + + // Read dir entries. + files, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("failed to read dir: %w", err) + } + + // Remove files older than threshold + deleteOlderThan := time.Now().Add(-threshold) + for _, f := range files { + // Skip directories and the current log file. + if f.IsDir() || f.Name() == currentLogFile { + continue + } + + // Delete log files. + if fileInfo, err := f.Info(); err == nil { + if fileInfo.ModTime().Before(deleteOlderThan) { + _ = os.Remove(filepath.Join(dir, f.Name())) + } + } + } + + return nil +} diff --git a/cmds/hub/main.go b/cmds/hub/main.go index 9c0379e8..1fdc8809 100644 --- a/cmds/hub/main.go +++ b/cmds/hub/main.go @@ -45,9 +45,8 @@ func main() { // Set SPN public hub mode. conf.EnablePublicHub(true) - // Set default log level. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() + // Start logger with default log level. + _ = log.Start(log.WarningLevel) // FIXME: Use service? diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go index a6ced810..598680f0 100644 --- a/cmds/observation-hub/main.go +++ b/cmds/observation-hub/main.go @@ -46,9 +46,8 @@ func main() { sluice.EnableListener = false api.EnableServer = false - // Set default log level. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() + // Start logger with default log level. + _ = log.Start(log.WarningLevel) // Create instance. var execCmdLine bool diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 0b4d0e01..8cd80864 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -24,6 +24,10 @@ var ( binDir string dataDir string + logToStdout bool + logDir string + logLevel string + svcCfg *service.ServiceConfig ) @@ -35,6 +39,11 @@ func init() { // Add persisent flags for all commands. rootCmd.PersistentFlags().StringVar(&binDir, "bin-dir", "", "set directory for executable binaries (rw/ro)") rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "set directory for variable data (rw)") + + // Add flags for service only. + rootCmd.Flags().BoolVar(&logToStdout, "log-stdout", false, "log to stdout instead of file") + rootCmd.Flags().StringVar(&logDir, "log-dir", "", "set directory for logs") + rootCmd.Flags().StringVar(&logLevel, "log", "", "set log level to [trace|debug|info|warning|error|critical]") } func main() { @@ -56,8 +65,13 @@ func initializeGlobals(cmd *cobra.Command, args []string) { // Create service config. svcCfg = &service.ServiceConfig{ - BinDir: binDir, - DataDir: dataDir, + BinDir: binDir, + DataDir: dataDir, + + LogToStdout: logToStdout, + LogDir: logDir, + LogLevel: logLevel, + BinariesIndexURLs: service.DefaultStableBinaryIndexURLs, IntelIndexURLs: service.DefaultIntelIndexURLs, VerifyBinaryUpdates: service.BinarySigningTrustStore, diff --git a/cmds/portmaster-core/run.go b/cmds/portmaster-core/run.go index 33702992..6c41bd56 100644 --- a/cmds/portmaster-core/run.go +++ b/cmds/portmaster-core/run.go @@ -75,9 +75,15 @@ func cmdRun(cmd *cobra.Command, args []string) { // START - // Set default log level and start logging. - log.SetLogLevel(log.WarningLevel) - _ = log.Start() + // FIXME: fix color and duplicate level when logging with slog + // FIXME: check for tty for color enabling + + // Start logging. + err = log.Start(svcCfg.LogLevel, svcCfg.LogToStdout, svcCfg.LogDir) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(4) + } // Create system service. service := NewSystemService(instance) @@ -97,10 +103,10 @@ func cmdRun(cmd *cobra.Command, args []string) { case <-instance.ShutdownComplete(): // Print stack on shutdown, if enabled. if printStackOnExit { - printStackTo(os.Stdout, "PRINTING STACK ON EXIT") + printStackTo(log.GlobalWriter, "PRINTING STACK ON EXIT") } case <-time.After(3 * time.Minute): - printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") + printStackTo(log.GlobalWriter, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN") } // Check if restart was triggered and send start service command if true. diff --git a/cmds/portmaster-core/run_linux.go b/cmds/portmaster-core/run_linux.go index 9b65b804..858ed022 100644 --- a/cmds/portmaster-core/run_linux.go +++ b/cmds/portmaster-core/run_linux.go @@ -11,6 +11,7 @@ import ( processInfo "github.com/shirou/gopsutil/process" "github.com/spf13/cobra" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/service" ) @@ -30,7 +31,7 @@ func (s *LinuxSystemService) Run() { // Print stack on start failure, if enabled. if printStackOnExit { - printStackTo(os.Stderr, "PRINTING STACK ON START FAILURE") + printStackTo(log.GlobalWriter, "PRINTING STACK ON START FAILURE") } os.Exit(1) @@ -57,7 +58,7 @@ wait: case sig := <-signalCh: // Only print and continue to wait if SIGUSR1 if sig == syscall.SIGUSR1 { - printStackTo(os.Stdout, "PRINTING STACK ON REQUEST") + printStackTo(log.GlobalWriter, "PRINTING STACK ON REQUEST") continue wait } else { // Trigger shutdown. @@ -84,7 +85,7 @@ wait: if forceCnt > 0 { fmt.Printf(" again, but already shutting down - %d more to force\n", sig, forceCnt) } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + printStackTo(log.GlobalWriter, "PRINTING STACK ON FORCED EXIT") os.Exit(1) } } diff --git a/cmds/portmaster-core/run_windows.go b/cmds/portmaster-core/run_windows.go index 3976745f..09593630 100644 --- a/cmds/portmaster-core/run_windows.go +++ b/cmds/portmaster-core/run_windows.go @@ -13,10 +13,12 @@ import ( "os/signal" "syscall" - "github.com/safing/portmaster/service" "github.com/spf13/cobra" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" + + "github.com/safing/portmaster/base/log" + "github.com/safing/portmaster/service" ) const serviceName = "PortmasterCore" @@ -66,7 +68,7 @@ func (s *WindowsSystemService) Execute(args []string, changeRequests <-chan svc. // Print stack on start failure, if enabled. if printStackOnExit { - printStackTo(os.Stderr, "PRINTING STACK ON START FAILURE") + printStackTo(log.GlobalWriter, "PRINTING STACK ON START FAILURE") } // Notify service manager we stopped again. @@ -140,7 +142,7 @@ waitShutdown: if forceCnt > 0 { fmt.Printf(" but already shutting down - %d more to force\n", sig, forceCnt) } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + printStackTo(log.GlobalWriter, "PRINTING STACK ON FORCED EXIT") os.Exit(1) } @@ -154,7 +156,7 @@ waitShutdown: if forceCnt > 0 { fmt.Printf(" but already shutting down - %d more to force\n", serviceCmdName(c.Cmd), forceCnt) } else { - printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT") + printStackTo(log.GlobalWriter, "PRINTING STACK ON FORCED EXIT") os.Exit(1) } diff --git a/cmds/portmaster-core/update.go b/cmds/portmaster-core/update.go index 31fcb881..866aab61 100644 --- a/cmds/portmaster-core/update.go +++ b/cmds/portmaster-core/update.go @@ -28,10 +28,11 @@ func update(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("internal configuration error: %w", err) } + // Force logging to stdout. + svcCfg.LogToStdout = true // Start logging. - log.SetLogLevel(log.InfoLevel) - _ = log.Start() + _ = log.Start(svcCfg.LogLevel, svcCfg.LogToStdout, svcCfg.LogDir) defer log.Shutdown() // Create updaters. diff --git a/go.mod b/go.mod index a97838af..eb548caa 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ go 1.22.0 replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2 require ( - fyne.io/systray v1.11.0 github.com/VictoriaMetrics/metrics v1.35.1 github.com/Xuanwo/go-locale v1.1.1 github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 @@ -19,11 +18,11 @@ require ( github.com/coreos/go-iptables v0.7.0 github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/badger v1.6.2 - github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 github.com/florianl/go-conntrack v0.4.0 github.com/florianl/go-nfqueue v1.3.2 github.com/fogleman/gg v1.3.0 github.com/ghodss/yaml v1.0.0 + github.com/gobwas/glob v0.2.3 github.com/godbus/dbus/v5 v5.1.0 github.com/gofrs/uuid v4.4.0+incompatible github.com/google/gopacket v1.1.19 @@ -34,9 +33,8 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/jackc/puddle/v2 v2.2.1 github.com/lmittmann/tint v1.0.5 + github.com/maruel/panicparse/v2 v2.3.1 github.com/mat/besticon v3.12.0+incompatible - github.com/mattn/go-colorable v0.1.13 - github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.62 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-server-timing v1.0.1 @@ -71,16 +69,12 @@ require ( require ( github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/aead/ecdh v0.2.0 // indirect - github.com/alessio/shellescape v1.4.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/danieljoos/wincred v1.2.1 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect github.com/golang/glog v1.2.1 // indirect @@ -91,7 +85,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/maruel/panicparse/v2 v2.3.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -113,7 +107,6 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zalando/go-keyring v0.2.5 // indirect github.com/zeebo/blake3 v0.2.4 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/mod v0.20.0 // indirect diff --git a/go.sum b/go.sum index 8f65cf10..ef8feec4 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= -fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -16,8 +14,6 @@ github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpY github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= -github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -43,8 +39,6 @@ github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= -github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -55,8 +49,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 h1:AnwbdEI8eV3GzLM3SlrJlYmYa6OB5X8RwY4A8QJOCP0= -github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435/go.mod h1:EMJ8XWTopp8OLRBMUm9vHE8Wn48CNpU21HM817OKNrc= github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls= github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf3x9T4= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -87,8 +79,6 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= -github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -181,11 +171,8 @@ github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1Lh github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= @@ -282,8 +269,6 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spkg/zipfs v0.7.1 h1:+2X5lvNHTybnDMQZAIHgedRXZK1WXdc+94R/P5v2XWE= github.com/spkg/zipfs v0.7.1/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -329,8 +314,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= -github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -420,7 +403,6 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/service/config.go b/service/config.go index 0d613315..7b7e0f08 100644 --- a/service/config.go +++ b/service/config.go @@ -8,12 +8,17 @@ import ( "runtime" "github.com/safing/jess" + "github.com/safing/portmaster/base/log" ) type ServiceConfig struct { BinDir string DataDir string + LogToStdout bool + LogDir string + LogLevel string + BinariesIndexURLs []string IntelIndexURLs []string VerifyBinaryUpdates jess.TrustStore @@ -21,9 +26,10 @@ type ServiceConfig struct { } func (sc *ServiceConfig) Init() error { - // Check directories + // Check directories. switch runtime.GOOS { case "windows": + // Fall back to defaults. if sc.BinDir == "" { exeDir, err := getCurrentBinaryFolder() // Default: C:/Program Files/Portmaster if err != nil { @@ -34,6 +40,9 @@ func (sc *ServiceConfig) Init() error { if sc.DataDir == "" { sc.DataDir = filepath.FromSlash("$ProgramData/Portmaster") } + if sc.LogDir == "" { + sc.LogDir = filepath.Join(sc.DataDir, "logs") + } case "linux": // Fall back to defaults. @@ -43,6 +52,9 @@ func (sc *ServiceConfig) Init() error { if sc.DataDir == "" { sc.DataDir = "/var/lib/portmaster" } + if sc.LogDir == "" { + sc.LogDir = "/var/log/portmaster" + } default: // Fail if not configured on other platforms. @@ -52,11 +64,15 @@ func (sc *ServiceConfig) Init() error { if sc.DataDir == "" { return errors.New("binary directory must be configured - auto-detection not supported on this platform") } + if !sc.LogToStdout && sc.LogDir == "" { + return errors.New("logging directory must be configured - auto-detection not supported on this platform") + } } // Expand path variables. sc.BinDir = os.ExpandEnv(sc.BinDir) sc.DataDir = os.ExpandEnv(sc.DataDir) + sc.LogDir = os.ExpandEnv(sc.LogDir) // Apply defaults for required fields. if len(sc.BinariesIndexURLs) == 0 { @@ -67,6 +83,11 @@ func (sc *ServiceConfig) Init() error { sc.IntelIndexURLs = DefaultIntelIndexURLs } + // Check log level. + if sc.LogLevel != "" && log.ParseLevel(sc.LogLevel) == 0 { + return fmt.Errorf("invalid log level %q", sc.LogLevel) + } + return nil } diff --git a/service/mgr/worker.go b/service/mgr/worker.go index ff06f05a..2151a2c8 100644 --- a/service/mgr/worker.go +++ b/service/mgr/worker.go @@ -5,11 +5,12 @@ import ( "errors" "fmt" "log/slog" - "os" "runtime" "runtime/debug" "strings" "time" + + "github.com/safing/portmaster/base/log" ) // workerContextKey is a key used for the context key/value storage. @@ -303,7 +304,7 @@ func (m *Manager) runWorker(w *WorkerCtx, fn func(w *WorkerCtx) error) (panicInf // Print panic to stderr. stackTrace := string(debug.Stack()) fmt.Fprintf( - os.Stderr, + log.GlobalWriter, "===== PANIC =====\n%s\n\n%s===== END =====\n", panicVal, stackTrace, diff --git a/spn/instance.go b/spn/instance.go index ce555d15..dcfe01aa 100644 --- a/spn/instance.go +++ b/spn/instance.go @@ -9,6 +9,7 @@ import ( "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/database/dbmodule" + "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/rng" @@ -92,6 +93,11 @@ func New() (*Instance, error) { // FIXME: fill } + // Initialize log + log.GlobalWriter = log.NewStdoutWriter() + + // FIXME: initialize log file. + var err error // Base modules