diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 00e9106..cac2fb0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -8,7 +8,9 @@ "build": "tsc && vite build", "preview": "vite preview", "tauri": "node ./scripts/tauri-with-stub.mjs", - "gen:icon": "node ./scripts/build-icon.mjs" + "gen:icon": "node ./scripts/build-icon.mjs", + "build:service": "cd service && cargo build --release", + "build:all": "bun run build:service && bun run tauri build" }, "dependencies": { "@radix-ui/react-scroll-area": "^1.2.3", diff --git a/apps/desktop/service/Cargo.lock b/apps/desktop/service/Cargo.lock new file mode 100644 index 0000000..da860fc --- /dev/null +++ b/apps/desktop/service/Cargo.lock @@ -0,0 +1,1931 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raven-service" +version = "0.1.0" +dependencies = [ + "chrono", + "interprocess", + "once_cell", + "parking_lot", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "windows", + "windows-service", + "winreg", +] + +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/apps/desktop/service/Cargo.toml b/apps/desktop/service/Cargo.toml new file mode 100644 index 0000000..a1334d5 --- /dev/null +++ b/apps/desktop/service/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "raven-service" +version = "0.1.0" +description = "Raven Windows Service - Executa operacoes privilegiadas para o Raven Desktop" +authors = ["Esdras Renan"] +edition = "2021" + +[[bin]] +name = "raven-service" +path = "src/main.rs" + +[dependencies] +# Windows Service +windows-service = "0.7" + +# Async runtime +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "io-util", "net", "signal"] } + +# IPC via Named Pipes +interprocess = { version = "2", features = ["tokio"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Windows Registry +winreg = "0.55" + +# Error handling +thiserror = "1.0" + +# HTTP client (para RustDesk) +reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking"], default-features = false } + +# Date/time +chrono = { version = "0.4", features = ["serde"] } + +# Crypto (para RustDesk ID) +sha2 = "0.10" + +# UUID para request IDs +uuid = { version = "1", features = ["v4"] } + +# Parking lot para locks +parking_lot = "0.12" + +# Once cell para singletons +once_cell = "1.19" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_Services", + "Win32_System_Threading", + "Win32_System_Pipes", + "Win32_System_IO", + "Win32_System_SystemServices", + "Win32_Storage_FileSystem", +] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true diff --git a/apps/desktop/service/src/ipc.rs b/apps/desktop/service/src/ipc.rs new file mode 100644 index 0000000..26091b6 --- /dev/null +++ b/apps/desktop/service/src/ipc.rs @@ -0,0 +1,290 @@ +//! Modulo IPC - Servidor de Named Pipes +//! +//! Implementa comunicacao entre o Raven UI e o Raven Service +//! usando Named Pipes do Windows com protocolo JSON-RPC simplificado. + +use crate::{rustdesk, usb_policy}; +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader, Write}; +use thiserror::Error; +use tracing::{debug, info, warn}; + +#[derive(Debug, Error)] +pub enum IpcError { + #[error("Erro de IO: {0}")] + Io(#[from] std::io::Error), + + #[error("Erro de serializacao: {0}")] + Json(#[from] serde_json::Error), +} + +/// Requisicao JSON-RPC simplificada +#[derive(Debug, Deserialize)] +pub struct Request { + pub id: String, + pub method: String, + #[serde(default)] + pub params: serde_json::Value, +} + +/// Resposta JSON-RPC simplificada +#[derive(Debug, Serialize)] +pub struct Response { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub code: i32, + pub message: String, +} + +impl Response { + pub fn success(id: String, result: serde_json::Value) -> Self { + Self { + id, + result: Some(result), + error: None, + } + } + + pub fn error(id: String, code: i32, message: String) -> Self { + Self { + id, + result: None, + error: Some(ErrorResponse { code, message }), + } + } +} + +/// Inicia o servidor de Named Pipes +pub async fn run_server(pipe_name: &str) -> Result<(), IpcError> { + info!("Iniciando servidor IPC em: {}", pipe_name); + + loop { + match accept_connection(pipe_name).await { + Ok(()) => { + debug!("Conexao processada com sucesso"); + } + Err(e) => { + warn!("Erro ao processar conexao: {}", e); + } + } + } +} + +/// Aceita uma conexao e processa requisicoes +async fn accept_connection(pipe_name: &str) -> Result<(), IpcError> { + use windows::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows::Win32::Security::{ + InitializeSecurityDescriptor, SetSecurityDescriptorDacl, + PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, + }; + use windows::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX; + use windows::Win32::System::Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, + PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, + }; + use windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION; + use windows::core::PCWSTR; + + // Cria o named pipe com seguranca que permite acesso a todos os usuarios + let pipe_name_wide: Vec = pipe_name.encode_utf16().chain(std::iter::once(0)).collect(); + + // Cria security descriptor com DACL nulo (permite acesso a todos) + let mut sd = SECURITY_DESCRIPTOR::default(); + unsafe { + let sd_ptr = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _); + let _ = InitializeSecurityDescriptor(sd_ptr, SECURITY_DESCRIPTOR_REVISION); + // DACL nulo = acesso irrestrito + let _ = SetSecurityDescriptorDacl(sd_ptr, true, None, false); + } + + let sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: &mut sd as *mut _ as *mut _, + bInheritHandle: false.into(), + }; + + let pipe_handle = unsafe { + CreateNamedPipeW( + PCWSTR::from_raw(pipe_name_wide.as_ptr()), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 4096, // out buffer + 4096, // in buffer + 0, // default timeout + Some(&sa), // seguranca permissiva + ) + }; + + // Verifica se o handle e valido + if pipe_handle == INVALID_HANDLE_VALUE { + return Err(IpcError::Io(std::io::Error::last_os_error())); + } + + // Aguarda conexao de um cliente + info!("Aguardando conexao de cliente..."); + let connect_result = unsafe { + ConnectNamedPipe(pipe_handle, None) + }; + + if let Err(e) = connect_result { + // ERROR_PIPE_CONNECTED (535) significa que o cliente ja estava conectado + // o que e aceitavel + let error_code = e.code().0 as u32; + if error_code != 535 { + warn!("Erro ao aguardar conexao: {:?}", e); + } + } + + info!("Cliente conectado"); + + // Processa requisicoes do cliente + let result = process_client(pipe_handle); + + // Desconecta o cliente + unsafe { + let _ = DisconnectNamedPipe(pipe_handle); + } + + result +} + +/// Processa requisicoes de um cliente conectado +fn process_client(pipe_handle: windows::Win32::Foundation::HANDLE) -> Result<(), IpcError> { + use std::os::windows::io::{FromRawHandle, RawHandle}; + use std::fs::File; + + // Cria File handle a partir do pipe + let raw_handle = pipe_handle.0 as RawHandle; + let file = unsafe { File::from_raw_handle(raw_handle) }; + + let reader = BufReader::new(file.try_clone()?); + let mut writer = file; + + // Le linhas (cada linha e uma requisicao JSON) + for line in reader.lines() { + let line = match line { + Ok(l) => l, + Err(e) => { + if e.kind() == std::io::ErrorKind::BrokenPipe { + info!("Cliente desconectou"); + break; + } + return Err(e.into()); + } + }; + + if line.is_empty() { + continue; + } + + debug!("Requisicao recebida: {}", line); + + // Parse da requisicao + let response = match serde_json::from_str::(&line) { + Ok(request) => handle_request(request), + Err(e) => Response::error( + "unknown".to_string(), + -32700, + format!("Parse error: {}", e), + ), + }; + + // Serializa e envia resposta + let response_json = serde_json::to_string(&response)?; + debug!("Resposta: {}", response_json); + + writeln!(writer, "{}", response_json)?; + writer.flush()?; + } + + // IMPORTANTE: Nao fechar o handle aqui, pois DisconnectNamedPipe precisa dele + std::mem::forget(writer); + + Ok(()) +} + +/// Processa uma requisicao e retorna a resposta +fn handle_request(request: Request) -> Response { + info!("Processando metodo: {}", request.method); + + match request.method.as_str() { + "health_check" => handle_health_check(request.id), + "apply_usb_policy" => handle_apply_usb_policy(request.id, request.params), + "get_usb_policy" => handle_get_usb_policy(request.id), + "provision_rustdesk" => handle_provision_rustdesk(request.id, request.params), + "get_rustdesk_status" => handle_get_rustdesk_status(request.id), + _ => Response::error( + request.id, + -32601, + format!("Metodo nao encontrado: {}", request.method), + ), + } +} + +// ============================================================================= +// Handlers de Requisicoes +// ============================================================================= + +fn handle_health_check(id: String) -> Response { + Response::success( + id, + serde_json::json!({ + "status": "ok", + "service": "RavenService", + "version": env!("CARGO_PKG_VERSION"), + "timestamp": chrono::Utc::now().timestamp_millis() + }), + ) +} + +fn handle_apply_usb_policy(id: String, params: serde_json::Value) -> Response { + let policy = match params.get("policy").and_then(|p| p.as_str()) { + Some(p) => p, + None => { + return Response::error(id, -32602, "Parametro 'policy' e obrigatorio".to_string()) + } + }; + + match usb_policy::apply_policy(policy) { + Ok(result) => Response::success(id, serde_json::to_value(result).unwrap()), + Err(e) => Response::error(id, -32000, format!("Erro ao aplicar politica: {}", e)), + } +} + +fn handle_get_usb_policy(id: String) -> Response { + match usb_policy::get_current_policy() { + Ok(policy) => Response::success( + id, + serde_json::json!({ + "policy": policy + }), + ), + Err(e) => Response::error(id, -32000, format!("Erro ao obter politica: {}", e)), + } +} + +fn handle_provision_rustdesk(id: String, params: serde_json::Value) -> Response { + let config_string = params.get("config").and_then(|c| c.as_str()).map(String::from); + let password = params.get("password").and_then(|p| p.as_str()).map(String::from); + let machine_id = params.get("machineId").and_then(|m| m.as_str()).map(String::from); + + match rustdesk::ensure_rustdesk(config_string.as_deref(), password.as_deref(), machine_id.as_deref()) { + Ok(result) => Response::success(id, serde_json::to_value(result).unwrap()), + Err(e) => Response::error(id, -32000, format!("Erro ao provisionar RustDesk: {}", e)), + } +} + +fn handle_get_rustdesk_status(id: String) -> Response { + match rustdesk::get_status() { + Ok(status) => Response::success(id, serde_json::to_value(status).unwrap()), + Err(e) => Response::error(id, -32000, format!("Erro ao obter status: {}", e)), + } +} diff --git a/apps/desktop/service/src/main.rs b/apps/desktop/service/src/main.rs new file mode 100644 index 0000000..208e22c --- /dev/null +++ b/apps/desktop/service/src/main.rs @@ -0,0 +1,268 @@ +//! Raven Service - Servico Windows para operacoes privilegiadas +//! +//! Este servico roda como LocalSystem e executa operacoes que requerem +//! privilegios de administrador, como: +//! - Aplicar politicas de USB +//! - Provisionar e configurar RustDesk +//! - Modificar chaves de registro em HKEY_LOCAL_MACHINE +//! +//! O app Raven UI comunica com este servico via Named Pipes. + +mod ipc; +mod rustdesk; +mod usb_policy; + +use std::ffi::OsString; +use std::time::Duration; +use tracing::{error, info}; +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, +}; + +const SERVICE_NAME: &str = "RavenService"; +const SERVICE_DISPLAY_NAME: &str = "Raven Desktop Service"; +const SERVICE_DESCRIPTION: &str = "Servico do Raven Desktop para operacoes privilegiadas (USB, RustDesk)"; +const PIPE_NAME: &str = r"\\.\pipe\RavenService"; + +define_windows_service!(ffi_service_main, service_main); + +fn main() -> Result<(), Box> { + // Configura logging + init_logging(); + + // Verifica argumentos de linha de comando + let args: Vec = std::env::args().collect(); + + if args.len() > 1 { + match args[1].as_str() { + "install" => { + install_service()?; + return Ok(()); + } + "uninstall" => { + uninstall_service()?; + return Ok(()); + } + "run" => { + // Modo de teste: roda sem registrar como servico + info!("Executando em modo de teste (nao como servico)"); + run_standalone()?; + return Ok(()); + } + _ => {} + } + } + + // Inicia como servico Windows + info!("Iniciando Raven Service..."); + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; + Ok(()) +} + +fn init_logging() { + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + + // Tenta criar diretorio de logs + let log_dir = std::env::var("PROGRAMDATA") + .map(|p| std::path::PathBuf::from(p).join("RavenService").join("logs")) + .unwrap_or_else(|_| std::path::PathBuf::from("C:\\ProgramData\\RavenService\\logs")); + + let _ = std::fs::create_dir_all(&log_dir); + + // Arquivo de log + let log_file = log_dir.join("service.log"); + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_file) + .ok(); + + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + + if let Some(file) = file { + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer().with_writer(file).with_ansi(false)) + .init(); + } else { + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer()) + .init(); + } +} + +fn service_main(arguments: Vec) { + if let Err(e) = run_service(arguments) { + error!("Erro ao executar servico: {}", e); + } +} + +fn run_service(_arguments: Vec) -> Result<(), Box> { + info!("Servico iniciando..."); + + // Canal para shutdown + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + let shutdown_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(shutdown_tx))); + + // Registra handler de controle do servico + let shutdown_tx_clone = shutdown_tx.clone(); + let status_handle = service_control_handler::register(SERVICE_NAME, move |control| { + match control { + ServiceControl::Stop | ServiceControl::Shutdown => { + info!("Recebido comando de parada"); + if let Ok(mut guard) = shutdown_tx_clone.lock() { + if let Some(tx) = guard.take() { + let _ = tx.send(()); + } + } + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + })?; + + // Atualiza status para Running + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + info!("Servico em execucao, aguardando conexoes..."); + + // Cria runtime Tokio + let runtime = tokio::runtime::Runtime::new()?; + + // Executa servidor IPC + runtime.block_on(async { + tokio::select! { + result = ipc::run_server(PIPE_NAME) => { + if let Err(e) = result { + error!("Erro no servidor IPC: {}", e); + } + } + _ = async { + let _ = shutdown_rx.await; + } => { + info!("Shutdown solicitado"); + } + } + }); + + // Atualiza status para Stopped + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + info!("Servico parado"); + Ok(()) +} + +fn run_standalone() -> Result<(), Box> { + let runtime = tokio::runtime::Runtime::new()?; + + runtime.block_on(async { + info!("Servidor IPC iniciando em modo standalone..."); + + tokio::select! { + result = ipc::run_server(PIPE_NAME) => { + if let Err(e) = result { + error!("Erro no servidor IPC: {}", e); + } + } + _ = tokio::signal::ctrl_c() => { + info!("Ctrl+C recebido, encerrando..."); + } + } + }); + + Ok(()) +} + +fn install_service() -> Result<(), Box> { + use windows_service::{ + service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType}, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + info!("Instalando servico..."); + + let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CREATE_SERVICE)?; + + let exe_path = std::env::current_exe()?; + + let service_info = ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY_NAME), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: exe_path, + launch_arguments: vec![], + dependencies: vec![], + account_name: None, // LocalSystem + account_password: None, + }; + + let service = manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; + + // Define descricao + service.set_description(SERVICE_DESCRIPTION)?; + + info!("Servico instalado com sucesso: {}", SERVICE_NAME); + println!("Servico '{}' instalado com sucesso!", SERVICE_DISPLAY_NAME); + println!("Para iniciar: sc start {}", SERVICE_NAME); + + Ok(()) +} + +fn uninstall_service() -> Result<(), Box> { + use windows_service::{ + service::ServiceAccess, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + info!("Desinstalando servico..."); + + let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?; + + let service = manager.open_service( + SERVICE_NAME, + ServiceAccess::STOP | ServiceAccess::DELETE | ServiceAccess::QUERY_STATUS, + )?; + + // Tenta parar o servico primeiro + let status = service.query_status()?; + if status.current_state != ServiceState::Stopped { + info!("Parando servico..."); + let _ = service.stop(); + std::thread::sleep(Duration::from_secs(2)); + } + + // Remove o servico + service.delete()?; + + info!("Servico desinstalado com sucesso"); + println!("Servico '{}' removido com sucesso!", SERVICE_DISPLAY_NAME); + + Ok(()) +} diff --git a/apps/desktop/service/src/rustdesk.rs b/apps/desktop/service/src/rustdesk.rs new file mode 100644 index 0000000..0df60aa --- /dev/null +++ b/apps/desktop/service/src/rustdesk.rs @@ -0,0 +1,846 @@ +//! Modulo RustDesk - Provisionamento e gerenciamento do RustDesk +//! +//! Gerencia a instalacao, configuracao e provisionamento do RustDesk. +//! Como o servico roda como LocalSystem, nao precisa de elevacao. + +use chrono::Utc; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::env; +use std::ffi::OsStr; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Write}; +use std::os::windows::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::Duration; +use thiserror::Error; +use tracing::{error, info, warn}; + +const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest"; +const USER_AGENT: &str = "RavenService/1.0"; +const SERVER_HOST: &str = "rust.rever.com.br"; +const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI="; +const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI> = Lazy::new(|| Mutex::new(())); + +#[derive(Debug, Error)] +pub enum RustdeskError { + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + #[error("Release asset nao encontrado para Windows x86_64")] + AssetMissing, + + #[error("Falha ao executar comando {command}: status {status:?}")] + CommandFailed { command: String, status: Option }, + + #[error("Falha ao detectar ID do RustDesk")] + MissingId, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskResult { + pub id: String, + pub password: String, + pub installed_version: Option, + pub updated: bool, + pub last_provisioned_at: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskStatus { + pub installed: bool, + pub running: bool, + pub id: Option, + pub version: Option, +} + +#[derive(Debug, Deserialize)] +struct ReleaseAsset { + name: String, + browser_download_url: String, +} + +#[derive(Debug, Deserialize)] +struct ReleaseResponse { + tag_name: String, + assets: Vec, +} + +/// Provisiona o RustDesk +pub fn ensure_rustdesk( + config_string: Option<&str>, + password_override: Option<&str>, + machine_id: Option<&str>, +) -> Result { + let _guard = PROVISION_MUTEX.lock(); + info!("Iniciando provisionamento do RustDesk"); + + // Prepara ACLs dos diretorios de servico + if let Err(e) = ensure_service_profiles_writable() { + warn!("Aviso ao preparar ACL: {}", e); + } + + // Le ID existente antes de qualquer limpeza + let preserved_remote_id = read_remote_id_from_profiles(); + if let Some(ref id) = preserved_remote_id { + info!("ID existente preservado: {}", id); + } + + let exe_path = detect_executable_path(); + let (installed_version, freshly_installed) = ensure_installed(&exe_path)?; + + info!( + "RustDesk {}: {}", + if freshly_installed { "instalado" } else { "ja presente" }, + exe_path.display() + ); + + // Para processos existentes + let _ = stop_rustdesk_processes(); + + // Limpa perfis apenas se instalacao fresca + if freshly_installed { + let _ = purge_existing_rustdesk_profiles(); + } + + // Aplica configuracao + if let Some(config) = config_string.filter(|c| !c.trim().is_empty()) { + if let Err(e) = run_with_args(&exe_path, &["--config", config]) { + warn!("Falha ao aplicar config inline: {}", e); + } + } else { + let config_path = write_config_files()?; + if let Err(e) = apply_config(&exe_path, &config_path) { + warn!("Falha ao aplicar config via CLI: {}", e); + } + } + + // Define senha + let password = password_override + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| DEFAULT_PASSWORD.to_string()); + + if let Err(e) = set_password(&exe_path, &password) { + warn!("Falha ao definir senha: {}", e); + } else { + let _ = ensure_password_files(&password); + let _ = propagate_password_profile(); + } + + // Define ID customizado + let custom_id = if let Some(ref existing_id) = preserved_remote_id { + if !freshly_installed { + Some(existing_id.clone()) + } else { + define_custom_id(&exe_path, machine_id) + } + } else { + define_custom_id(&exe_path, machine_id) + }; + + // Inicia servico + if let Err(e) = ensure_service_running(&exe_path) { + warn!("Falha ao iniciar servico: {}", e); + } + + // Obtem ID final + let final_id = match query_id_with_retries(&exe_path, 5) { + Ok(id) => id, + Err(_) => { + read_remote_id_from_profiles() + .or_else(|| custom_id.clone()) + .ok_or(RustdeskError::MissingId)? + } + }; + + // Garante ID em todos os arquivos + ensure_remote_id_files(&final_id); + + let version = query_version(&exe_path).ok().or(installed_version); + let last_provisioned_at = Utc::now().timestamp_millis(); + + info!("Provisionamento concluido. ID: {}, Versao: {:?}", final_id, version); + + Ok(RustdeskResult { + id: final_id, + password, + installed_version: version, + updated: freshly_installed, + last_provisioned_at, + }) +} + +/// Retorna status do RustDesk +pub fn get_status() -> Result { + let exe_path = detect_executable_path(); + let installed = exe_path.exists(); + + let running = if installed { + query_service_state().map(|s| s == "running").unwrap_or(false) + } else { + false + }; + + let id = if installed { + query_id(&exe_path).ok().or_else(read_remote_id_from_profiles) + } else { + None + }; + + let version = if installed { + query_version(&exe_path).ok() + } else { + None + }; + + Ok(RustdeskStatus { + installed, + running, + id, + version, + }) +} + +// ============================================================================= +// Funcoes Auxiliares +// ============================================================================= + +fn detect_executable_path() -> PathBuf { + let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string()); + Path::new(&program_files).join("RustDesk").join("rustdesk.exe") +} + +fn ensure_installed(exe_path: &Path) -> Result<(Option, bool), RustdeskError> { + if exe_path.exists() { + return Ok((None, false)); + } + + let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) + .join(CACHE_DIR_NAME); + fs::create_dir_all(&cache_root)?; + + let (installer_path, version_tag) = download_latest_installer(&cache_root)?; + run_installer(&installer_path)?; + thread::sleep(Duration::from_secs(20)); + + Ok((Some(version_tag), true)) +} + +fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> { + let client = Client::builder() + .user_agent(USER_AGENT) + .timeout(Duration::from_secs(60)) + .build()?; + + let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?; + + let asset = release + .assets + .iter() + .find(|a| a.name.ends_with("x86_64.exe")) + .ok_or(RustdeskError::AssetMissing)?; + + let target_path = cache_root.join(&asset.name); + if target_path.exists() { + return Ok((target_path, release.tag_name)); + } + + info!("Baixando RustDesk: {}", asset.name); + let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?; + let mut output = File::create(&target_path)?; + response.copy_to(&mut output)?; + + Ok((target_path, release.tag_name)) +} + +fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> { + let status = hidden_command(installer_path) + .arg("--silent-install") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --silent-install", installer_path.display()), + status: status.code(), + }); + } + Ok(()) +} + +fn program_data_config_dir() -> PathBuf { + PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) + .join("RustDesk") + .join("config") +} + +/// Retorna todos os diretorios AppData\Roaming\RustDesk\config de usuarios do sistema +/// Como o servico roda como LocalSystem, precisamos enumerar os profiles de usuarios +fn all_user_appdata_config_dirs() -> Vec { + let mut dirs = Vec::new(); + + // Enumera C:\Users\*\AppData\Roaming\RustDesk\config + let users_dir = Path::new("C:\\Users"); + if let Ok(entries) = fs::read_dir(users_dir) { + for entry in entries.flatten() { + let path = entry.path(); + // Ignora pastas de sistema + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name == "Public" || name == "Default" || name == "Default User" || name == "All Users" { + continue; + } + let rustdesk_config = path.join("AppData").join("Roaming").join("RustDesk").join("config"); + // Verifica se o diretorio pai existe (usuario real) + if path.join("AppData").join("Roaming").exists() { + dirs.push(rustdesk_config); + } + } + } + + // Tambem tenta o APPDATA do ambiente (pode ser util em alguns casos) + if let Ok(appdata) = env::var("APPDATA") { + let path = Path::new(&appdata).join("RustDesk").join("config"); + if !dirs.contains(&path) { + dirs.push(path); + } + } + + dirs +} + +fn service_profile_dirs() -> Vec { + vec![ + PathBuf::from(LOCAL_SERVICE_CONFIG), + PathBuf::from(LOCAL_SYSTEM_CONFIG), + ] +} + +fn remote_id_directories() -> Vec { + let mut dirs = Vec::new(); + dirs.push(program_data_config_dir()); + dirs.extend(service_profile_dirs()); + dirs.extend(all_user_appdata_config_dirs()); + dirs +} + +fn write_config_files() -> Result { + let config_contents = format!( + r#"[options] +key = "{key}" +relay-server = "{host}" +custom-rendezvous-server = "{host}" +api-server = "https://{host}" +verification-method = "{verification}" +approve-mode = "{approve}" +"#, + host = SERVER_HOST, + key = SERVER_KEY, + verification = SECURITY_VERIFICATION_VALUE, + approve = SECURITY_APPROVE_MODE_VALUE, + ); + + let main_path = program_data_config_dir().join("RustDesk2.toml"); + write_file(&main_path, &config_contents)?; + + for service_dir in service_profile_dirs() { + let service_profile = service_dir.join("RustDesk2.toml"); + let _ = write_file(&service_profile, &config_contents); + } + + Ok(main_path) +} + +fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(contents.as_bytes()) +} + +fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> { + run_with_args(exe_path, &["--import-config", &config_path.to_string_lossy()]) +} + +fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> { + run_with_args(exe_path, &["--password", secret]) +} + +fn define_custom_id(exe_path: &Path, machine_id: Option<&str>) -> Option { + let value = machine_id.and_then(|raw| { + let trimmed = raw.trim(); + if trimmed.is_empty() { None } else { Some(trimmed) } + })?; + + let custom_id = derive_numeric_id(value); + if run_with_args(exe_path, &["--set-id", &custom_id]).is_ok() { + info!("ID deterministico definido: {}", custom_id); + Some(custom_id) + } else { + None + } +} + +fn derive_numeric_id(machine_id: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(machine_id.as_bytes()); + let hash = hasher.finalize(); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&hash[..8]); + let value = u64::from_le_bytes(bytes); + let num = (value % 900_000_000) + 100_000_000; + format!("{:09}", num) +} + +fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> { + ensure_service_installed(exe_path)?; + let _ = run_sc(&["config", SERVICE_NAME, "start=", "auto"]); + let _ = run_sc(&["start", SERVICE_NAME]); + remove_rustdesk_autorun_artifacts(); + Ok(()) +} + +fn ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> { + if run_sc(&["query", SERVICE_NAME]).is_ok() { + return Ok(()); + } + run_with_args(exe_path, &["--install-service"]) +} + +fn stop_rustdesk_processes() -> Result<(), RustdeskError> { + let _ = run_sc(&["stop", SERVICE_NAME]); + thread::sleep(Duration::from_secs(2)); + + let status = hidden_command("taskkill") + .args(["/F", "/T", "/IM", "rustdesk.exe"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + if status.success() || matches!(status.code(), Some(128)) { + Ok(()) + } else { + Err(RustdeskError::CommandFailed { + command: "taskkill".into(), + status: status.code(), + }) + } +} + +fn purge_existing_rustdesk_profiles() -> Result<(), String> { + let files = [ + "RustDesk.toml", + "RustDesk_local.toml", + "RustDesk2.toml", + "password", + "passwd", + "passwd.txt", + ]; + + for dir in remote_id_directories() { + if !dir.exists() { + continue; + } + for name in files { + let path = dir.join(name); + if path.exists() { + let _ = fs::remove_file(&path); + } + } + } + Ok(()) +} + +fn ensure_password_files(secret: &str) -> Result<(), String> { + for dir in remote_id_directories() { + let password_path = dir.join("RustDesk.toml"); + let _ = write_toml_kv(&password_path, "password", secret); + + let local_path = dir.join("RustDesk_local.toml"); + let _ = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE); + let _ = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE); + } + Ok(()) +} + +fn propagate_password_profile() -> io::Result { + // Encontra um diretorio de usuario que tenha arquivos de config + let user_dirs = all_user_appdata_config_dirs(); + let src_dir = user_dirs.iter().find(|d| d.join("RustDesk.toml").exists()); + + let Some(src_dir) = src_dir else { + // Se nenhum usuario tem config, usa ProgramData como fonte + let pd = program_data_config_dir(); + if !pd.join("RustDesk.toml").exists() { + return Ok(false); + } + return propagate_from_dir(&pd); + }; + + propagate_from_dir(src_dir) +} + +fn propagate_from_dir(src_dir: &Path) -> io::Result { + let propagation_files = ["RustDesk.toml", "RustDesk_local.toml", "RustDesk2.toml"]; + let mut propagated = false; + + for filename in propagation_files { + let src_path = src_dir.join(filename); + if !src_path.exists() { + continue; + } + + for dest_root in remote_id_directories() { + if dest_root == src_dir { + continue; // Nao copiar para si mesmo + } + let target_path = dest_root.join(filename); + if copy_overwrite(&src_path, &target_path).is_ok() { + propagated = true; + } + } + } + + Ok(propagated) +} + +fn ensure_remote_id_files(id: &str) { + for dir in remote_id_directories() { + let path = dir.join("RustDesk_local.toml"); + let _ = write_remote_id_value(&path, id); + } +} + +fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let replacement = format!("remote_id = '{}'\n", id); + if let Ok(existing) = fs::read_to_string(path) { + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + if line.trim_start().starts_with("remote_id") { + buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); + } + } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) + } else { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(replacement.as_bytes()) + } +} + +fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let sanitized = value.replace('\\', "\\\\").replace('"', "\\\""); + let replacement = format!("{key} = \"{sanitized}\"\n"); + let existing = fs::read_to_string(path).unwrap_or_default(); + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) { + buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); + } + } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) +} + +fn read_remote_id_from_profiles() -> Option { + for dir in remote_id_directories() { + for candidate in [dir.join("RustDesk_local.toml"), dir.join("RustDesk.toml")] { + if let Some(id) = read_remote_id_file(&candidate) { + if !id.is_empty() { + return Some(id); + } + } + } + } + None +} + +fn read_remote_id_file(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some(value) = parse_assignment(line, "remote_id") { + return Some(value); + } + } + None +} + +fn parse_assignment(line: &str, key: &str) -> Option { + let trimmed = line.trim(); + if !trimmed.starts_with(key) { + return None; + } + let (_, rhs) = trimmed.split_once('=')?; + let value = rhs.trim().trim_matches(|c| c == '\'' || c == '"'); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result { + for attempt in 0..attempts { + match query_id(exe_path) { + Ok(value) if !value.trim().is_empty() => return Ok(value), + _ => {} + } + if attempt + 1 < attempts { + thread::sleep(Duration::from_millis(800)); + } + } + Err(RustdeskError::MissingId) +} + +fn query_id(exe_path: &Path) -> Result { + let output = hidden_command(exe_path).arg("--get-id").output()?; + if !output.status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --get-id", exe_path.display()), + status: output.status.code(), + }); + } + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if stdout.is_empty() { + return Err(RustdeskError::MissingId); + } + Ok(stdout) +} + +fn query_version(exe_path: &Path) -> Result { + let output = hidden_command(exe_path).arg("--version").output()?; + if !output.status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} --version", exe_path.display()), + status: output.status.code(), + }); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn query_service_state() -> Option { + let output = hidden_command("sc") + .args(["query", SERVICE_NAME]) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let lower = line.to_lowercase(); + if lower.contains("running") { + return Some("running".to_string()); + } + if lower.contains("stopped") { + return Some("stopped".to_string()); + } + } + None +} + +fn run_sc(args: &[&str]) -> Result<(), RustdeskError> { + let status = hidden_command("sc") + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("sc {}", args.join(" ")), + status: status.code(), + }); + } + Ok(()) +} + +fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> { + let status = hidden_command(exe_path) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + if !status.success() { + return Err(RustdeskError::CommandFailed { + command: format!("{} {}", exe_path.display(), args.join(" ")), + status: status.code(), + }); + } + Ok(()) +} + +fn remove_rustdesk_autorun_artifacts() { + // Remove atalhos de inicializacao automatica + let mut startup_paths: Vec = Vec::new(); + if let Ok(appdata) = env::var("APPDATA") { + startup_paths.push( + Path::new(&appdata) + .join("Microsoft\\Windows\\Start Menu\\Programs\\Startup\\RustDesk.lnk"), + ); + } + startup_paths.push(PathBuf::from( + r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\RustDesk.lnk", + )); + + for path in startup_paths { + if path.exists() { + let _ = fs::remove_file(&path); + } + } + + // Remove entradas de registro + for hive in ["HKCU", "HKLM"] { + let reg_path = format!(r"{}\Software\Microsoft\Windows\CurrentVersion\Run", hive); + let _ = hidden_command("reg") + .args(["delete", ®_path, "/v", "RustDesk", "/f"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +fn ensure_service_profiles_writable() -> Result<(), String> { + for dir in service_profile_dirs() { + if !can_write_dir(&dir) { + fix_profile_acl(&dir)?; + } + } + Ok(()) +} + +fn can_write_dir(dir: &Path) -> bool { + if fs::create_dir_all(dir).is_err() { + return false; + } + let probe = dir.join(".raven_acl_probe"); + match OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&probe) + { + Ok(mut file) => { + if file.write_all(b"ok").is_err() { + let _ = fs::remove_file(&probe); + return false; + } + let _ = fs::remove_file(&probe); + true + } + Err(_) => false, + } +} + +fn fix_profile_acl(target: &Path) -> Result<(), String> { + let target_str = target.display().to_string(); + + // Como ja estamos rodando como LocalSystem, podemos usar takeown/icacls diretamente + let _ = hidden_command("takeown") + .args(["/F", &target_str, "/R", "/D", "Y"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + let status = hidden_command("icacls") + .args([ + &target_str, + "/grant", + "*S-1-5-32-544:(OI)(CI)F", + "*S-1-5-19:(OI)(CI)F", + "*S-1-5-32-545:(OI)(CI)M", + "/T", + "/C", + "/Q", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| format!("Erro ao executar icacls: {}", e))?; + + if status.success() { + Ok(()) + } else { + Err(format!("icacls retornou codigo {}", status.code().unwrap_or(-1))) + } +} + +fn copy_overwrite(src: &Path, dst: &Path) -> io::Result<()> { + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent)?; + } + if dst.is_dir() { + fs::remove_dir_all(dst)?; + } else if dst.exists() { + fs::remove_file(dst)?; + } + fs::copy(src, dst)?; + Ok(()) +} + +fn hidden_command(program: impl AsRef) -> Command { + let mut cmd = Command::new(program); + cmd.creation_flags(CREATE_NO_WINDOW); + cmd +} diff --git a/apps/desktop/service/src/usb_policy.rs b/apps/desktop/service/src/usb_policy.rs new file mode 100644 index 0000000..ed8144d --- /dev/null +++ b/apps/desktop/service/src/usb_policy.rs @@ -0,0 +1,259 @@ +//! Modulo USB Policy - Controle de dispositivos USB +//! +//! Implementa o controle de armazenamento USB no Windows. +//! Como o servico roda como LocalSystem, nao precisa de elevacao. + +use serde::{Deserialize, Serialize}; +use std::io; +use thiserror::Error; +use tracing::{error, info, warn}; +use winreg::enums::*; +use winreg::RegKey; + +// GUID para Removable Storage Devices (Disk) +const REMOVABLE_STORAGE_GUID: &str = "{53f56307-b6bf-11d0-94f2-00a0c91efb8b}"; + +// Chaves de registro +const REMOVABLE_STORAGE_PATH: &str = r"Software\Policies\Microsoft\Windows\RemovableStorageDevices"; +const USBSTOR_PATH: &str = r"SYSTEM\CurrentControlSet\Services\USBSTOR"; +const STORAGE_POLICY_PATH: &str = r"SYSTEM\CurrentControlSet\Control\StorageDevicePolicies"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum UsbPolicy { + Allow, + BlockAll, + Readonly, +} + +impl UsbPolicy { + pub fn from_str(s: &str) -> Option { + match s.to_uppercase().as_str() { + "ALLOW" => Some(Self::Allow), + "BLOCK_ALL" => Some(Self::BlockAll), + "READONLY" => Some(Self::Readonly), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Allow => "ALLOW", + Self::BlockAll => "BLOCK_ALL", + Self::Readonly => "READONLY", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsbPolicyResult { + pub success: bool, + pub policy: String, + pub error: Option, + pub applied_at: Option, +} + +#[derive(Error, Debug)] +pub enum UsbControlError { + #[error("Politica USB invalida: {0}")] + InvalidPolicy(String), + + #[error("Erro de registro do Windows: {0}")] + RegistryError(String), + + #[error("Permissao negada")] + PermissionDenied, + + #[error("Erro de I/O: {0}")] + Io(#[from] io::Error), +} + +/// Aplica uma politica de USB +pub fn apply_policy(policy_str: &str) -> Result { + let policy = UsbPolicy::from_str(policy_str) + .ok_or_else(|| UsbControlError::InvalidPolicy(policy_str.to_string()))?; + + let now = chrono::Utc::now().timestamp_millis(); + + info!("Aplicando politica USB: {:?}", policy); + + // 1. Aplicar Removable Storage Policy + apply_removable_storage_policy(policy)?; + + // 2. Aplicar USBSTOR + apply_usbstor_policy(policy)?; + + // 3. Aplicar WriteProtect se necessario + if policy == UsbPolicy::Readonly { + apply_write_protect(true)?; + } else { + apply_write_protect(false)?; + } + + // 4. Atualizar Group Policy (opcional) + if let Err(e) = refresh_group_policy() { + warn!("Falha ao atualizar group policy: {}", e); + } + + info!("Politica USB aplicada com sucesso: {:?}", policy); + + Ok(UsbPolicyResult { + success: true, + policy: policy.as_str().to_string(), + error: None, + applied_at: Some(now), + }) +} + +/// Retorna a politica USB atual +pub fn get_current_policy() -> Result { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + // Verifica Removable Storage Policy primeiro + let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID); + + if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_READ) { + let deny_read: u32 = key.get_value("Deny_Read").unwrap_or(0); + let deny_write: u32 = key.get_value("Deny_Write").unwrap_or(0); + + if deny_read == 1 && deny_write == 1 { + return Ok("BLOCK_ALL".to_string()); + } + + if deny_read == 0 && deny_write == 1 { + return Ok("READONLY".to_string()); + } + } + + // Verifica USBSTOR como fallback + if let Ok(key) = hklm.open_subkey_with_flags(USBSTOR_PATH, KEY_READ) { + let start: u32 = key.get_value("Start").unwrap_or(3); + if start == 4 { + return Ok("BLOCK_ALL".to_string()); + } + } + + Ok("ALLOW".to_string()) +} + +fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID); + + match policy { + UsbPolicy::Allow => { + // Tenta remover as restricoes, se existirem + if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_ALL_ACCESS) { + let _ = key.delete_value("Deny_Read"); + let _ = key.delete_value("Deny_Write"); + let _ = key.delete_value("Deny_Execute"); + } + // Tenta remover a chave inteira se estiver vazia + let _ = hklm.delete_subkey(&full_path); + } + UsbPolicy::BlockAll => { + let (key, _) = hklm + .create_subkey(&full_path) + .map_err(map_winreg_error)?; + + key.set_value("Deny_Read", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Write", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Execute", &1u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::Readonly => { + let (key, _) = hklm + .create_subkey(&full_path) + .map_err(map_winreg_error)?; + + // Permite leitura, bloqueia escrita + key.set_value("Deny_Read", &0u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Write", &1u32) + .map_err(map_winreg_error)?; + key.set_value("Deny_Execute", &0u32) + .map_err(map_winreg_error)?; + } + } + + Ok(()) +} + +fn apply_usbstor_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + let key = hklm + .open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS) + .map_err(map_winreg_error)?; + + match policy { + UsbPolicy::Allow => { + // Start = 3 habilita o driver + key.set_value("Start", &3u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::BlockAll => { + // Start = 4 desabilita o driver + key.set_value("Start", &4u32) + .map_err(map_winreg_error)?; + } + UsbPolicy::Readonly => { + // Readonly mantem driver ativo + key.set_value("Start", &3u32) + .map_err(map_winreg_error)?; + } + } + + Ok(()) +} + +fn apply_write_protect(enable: bool) -> Result<(), UsbControlError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + if enable { + let (key, _) = hklm + .create_subkey(STORAGE_POLICY_PATH) + .map_err(map_winreg_error)?; + + key.set_value("WriteProtect", &1u32) + .map_err(map_winreg_error)?; + } else if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) { + let _ = key.set_value("WriteProtect", &0u32); + } + + Ok(()) +} + +fn refresh_group_policy() -> Result<(), UsbControlError> { + use std::os::windows::process::CommandExt; + use std::process::Command; + + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let output = Command::new("gpupdate") + .args(["/target:computer", "/force"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(UsbControlError::Io)?; + + if !output.status.success() { + warn!( + "gpupdate retornou erro: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(()) +} + +fn map_winreg_error(error: io::Error) -> UsbControlError { + if let Some(code) = error.raw_os_error() { + if code == 5 { + return UsbControlError::PermissionDenied; + } + } + UsbControlError::RegistryError(error.to_string()) +} diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 86e04da..a3a293a 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -80,10 +80,12 @@ dependencies = [ "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-process", + "tauri-plugin-single-instance", "tauri-plugin-store", "tauri-plugin-updater", "thiserror 1.0.69", "tokio", + "uuid", "winreg", ] @@ -4748,6 +4750,21 @@ dependencies = [ "tauri-plugin", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.17", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-store" version = "2.4.0" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index efa7052..944e0d3 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -26,6 +26,7 @@ tauri-plugin-updater = "2.9.0" tauri-plugin-process = "2.3.0" tauri-plugin-notification = "2" tauri-plugin-deep-link = "2" +tauri-plugin-single-instance = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] } @@ -41,6 +42,7 @@ hostname = "0.4" base64 = "0.22" sha2 = "0.10" convex = "0.10.2" +uuid = { version = "1", features = ["v4"] } # SSE usa reqwest com stream, nao precisa de websocket [target.'cfg(windows)'.dependencies] diff --git a/apps/desktop/src-tauri/installer-hooks.nsh b/apps/desktop/src-tauri/installer-hooks.nsh index f6e32c6..a716376 100644 --- a/apps/desktop/src-tauri/installer-hooks.nsh +++ b/apps/desktop/src-tauri/installer-hooks.nsh @@ -1,20 +1,97 @@ ; Hooks customizadas do instalador NSIS (Tauri) ; -; Objetivo: remover a marca "Nullsoft Install System" exibida no canto inferior esquerdo. +; Objetivo: +; - Remover a marca "Nullsoft Install System" exibida no canto inferior esquerdo +; - Instalar o Raven Service para operacoes privilegiadas sem UAC ; ; Nota: o bundler do Tauri injeta estes macros no script principal do instalador. BrandingText " " !macro NSIS_HOOK_PREINSTALL + ; Para qualquer instancia anterior do servico antes de atualizar + DetailPrint "Parando servicos anteriores..." + + ; Para o servico + nsExec::ExecToLog 'sc stop RavenService' + + ; Aguarda o servico parar completamente (ate 10 segundos) + nsExec::ExecToLog 'powershell -Command "$$i=0; while((Get-Service RavenService -ErrorAction SilentlyContinue).Status -eq \"Running\" -and $$i -lt 10){Start-Sleep 1;$$i++}"' + + ; Forca encerramento de processos remanescentes + nsExec::ExecToLog 'taskkill /F /IM raven-service.exe' + nsExec::ExecToLog 'taskkill /F /IM appsdesktop.exe' + + ; Aguarda liberacao dos arquivos + Sleep 2000 !macroend !macro NSIS_HOOK_POSTINSTALL + ; ========================================================================= + ; Instala e inicia o Raven Service + ; ========================================================================= + + DetailPrint "Instalando Raven Service..." + + ; O servico ja esta em $INSTDIR (copiado como resource pelo Tauri) + ; Registra o servico Windows + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" install' + Pop $0 + + ${If} $0 != 0 + DetailPrint "Aviso: Falha ao registrar servico (codigo: $0)" + ; Tenta remover e reinstalar + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" uninstall' + Sleep 500 + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" install' + Pop $0 + ${EndIf} + + ; Inicia o servico + DetailPrint "Iniciando Raven Service..." + nsExec::ExecToLog 'sc start RavenService' + Pop $0 + + ${If} $0 == 0 + DetailPrint "Raven Service iniciado com sucesso!" + ${Else} + DetailPrint "Aviso: Servico sera iniciado na proxima reinicializacao" + ${EndIf} + + ; ========================================================================= + ; Verifica se RustDesk esta instalado + ; Se nao estiver, o Raven Service instalara automaticamente no primeiro uso + ; ========================================================================= + + IfFileExists "$PROGRAMFILES\RustDesk\rustdesk.exe" rustdesk_found rustdesk_not_found + + rustdesk_not_found: + DetailPrint "RustDesk sera instalado automaticamente pelo Raven Service." + Goto rustdesk_done + + rustdesk_found: + DetailPrint "RustDesk ja esta instalado." + + rustdesk_done: !macroend !macro NSIS_HOOK_PREUNINSTALL + ; ========================================================================= + ; Para e remove o Raven Service + ; ========================================================================= + + DetailPrint "Parando Raven Service..." + nsExec::ExecToLog 'sc stop RavenService' + Sleep 1000 + + DetailPrint "Removendo Raven Service..." + nsExec::ExecToLog '"$INSTDIR\raven-service.exe" uninstall' + + ; Aguarda um pouco para garantir que o servico foi removido + Sleep 500 !macroend !macro NSIS_HOOK_POSTUNINSTALL + ; Nada adicional necessario !macroend diff --git a/apps/desktop/src-tauri/src/agent.rs b/apps/desktop/src-tauri/src/agent.rs index b663005..4fbb53d 100644 --- a/apps/desktop/src-tauri/src/agent.rs +++ b/apps/desktop/src-tauri/src/agent.rs @@ -708,7 +708,7 @@ fn collect_windows_extended() -> serde_json::Value { } fn decode_utf16_le_to_string(bytes: &[u8]) -> Option { - if bytes.len() % 2 != 0 { + if !bytes.len().is_multiple_of(2) { return None; } let utf16: Vec = bytes @@ -1086,7 +1086,7 @@ pub fn collect_profile() -> Result { let system = collect_system(); let os_name = System::name() - .or_else(|| System::long_os_version()) + .or_else(System::long_os_version) .unwrap_or_else(|| "desconhecido".to_string()); let os_version = System::os_version(); let architecture = std::env::consts::ARCH.to_string(); @@ -1146,7 +1146,7 @@ async fn post_heartbeat( .into_owned(); let os = MachineOs { name: System::name() - .or_else(|| System::long_os_version()) + .or_else(System::long_os_version) .unwrap_or_else(|| "desconhecido".to_string()), version: System::os_version(), architecture: Some(std::env::consts::ARCH.to_string()), diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index ece1ae8..2b3d54b 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2,6 +2,8 @@ mod agent; mod chat; #[cfg(target_os = "windows")] mod rustdesk; +#[cfg(target_os = "windows")] +mod service_client; mod usb_control; use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile}; @@ -68,21 +70,21 @@ pub fn log_agent(level: &str, message: &str) { #[macro_export] macro_rules! log_info { ($($arg:tt)*) => { - $crate::log_agent("INFO", &format!($($arg)*)) + $crate::log_agent("INFO", format!($($arg)*).as_str()) }; } #[macro_export] macro_rules! log_error { ($($arg:tt)*) => { - $crate::log_agent("ERROR", &format!($($arg)*)) + $crate::log_agent("ERROR", format!($($arg)*).as_str()) }; } #[macro_export] macro_rules! log_warn { ($($arg:tt)*) => { - $crate::log_agent("WARN", &format!($($arg)*)) + $crate::log_agent("WARN", format!($($arg)*).as_str()) }; } @@ -189,6 +191,32 @@ fn run_rustdesk_ensure( password: Option, machine_id: Option, ) -> Result { + // Tenta usar o servico primeiro (sem UAC) + if service_client::is_service_available() { + log_info!("Usando Raven Service para provisionar RustDesk"); + match service_client::provision_rustdesk( + config_string.as_deref(), + password.as_deref(), + machine_id.as_deref(), + ) { + Ok(result) => { + return Ok(RustdeskProvisioningResult { + id: result.id, + password: result.password, + installed_version: result.installed_version, + updated: result.updated, + last_provisioned_at: result.last_provisioned_at, + }); + } + Err(e) => { + log_warn!("Falha ao usar servico para RustDesk: {e}"); + // Continua para fallback + } + } + } + + // Fallback: chamada direta (pode pedir UAC) + log_info!("Usando chamada direta para provisionar RustDesk (pode pedir UAC)"); rustdesk::ensure_rustdesk( config_string.as_deref(), password.as_deref(), @@ -208,14 +236,50 @@ fn run_rustdesk_ensure( #[tauri::command] fn apply_usb_policy(policy: String) -> Result { - let policy_enum = UsbPolicy::from_str(&policy) + // Valida a politica primeiro + let _policy_enum = UsbPolicy::from_str(&policy) .ok_or_else(|| format!("Politica USB invalida: {}. Use ALLOW, BLOCK_ALL ou READONLY.", policy))?; - usb_control::apply_usb_policy(policy_enum).map_err(|e| e.to_string()) + // Tenta usar o servico primeiro (sem UAC) + #[cfg(target_os = "windows")] + if service_client::is_service_available() { + log_info!("Usando Raven Service para aplicar politica USB: {}", policy); + match service_client::apply_usb_policy(&policy) { + Ok(result) => { + return Ok(UsbPolicyResult { + success: result.success, + policy: result.policy, + error: result.error, + applied_at: result.applied_at, + }); + } + Err(e) => { + log_warn!("Falha ao usar servico para USB policy: {e}"); + // Continua para fallback + } + } + } + + // Fallback: chamada direta (pode pedir UAC) + log_info!("Usando chamada direta para aplicar politica USB (pode pedir UAC)"); + usb_control::apply_usb_policy(_policy_enum).map_err(|e| e.to_string()) } #[tauri::command] fn get_usb_policy() -> Result { + // Tenta usar o servico primeiro + #[cfg(target_os = "windows")] + if service_client::is_service_available() { + match service_client::get_usb_policy() { + Ok(policy) => return Ok(policy), + Err(e) => { + log_warn!("Falha ao obter USB policy via servico: {e}"); + // Continua para fallback + } + } + } + + // Fallback: leitura direta (nao precisa elevacao para ler) usb_control::get_current_policy() .map(|p| p.as_str().to_string()) .map_err(|e| e.to_string()) @@ -452,6 +516,14 @@ pub fn run() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + // Quando uma segunda instância tenta iniciar, foca a janela existente + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } + })) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); @@ -481,7 +553,7 @@ pub fn run() { { let start_in_background = std::env::args().any(|arg| arg == "--background"); setup_raven_autostart(); - setup_tray(&app.handle())?; + setup_tray(app.handle())?; if start_in_background { if let Some(win) = app.get_webview_window("main") { let _ = win.hide(); diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index ef4a81f..8c6cee4 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -1,5 +1,3 @@ -#![cfg(target_os = "windows")] - use crate::RustdeskProvisioningResult; use chrono::{Local, Utc}; use once_cell::sync::Lazy; @@ -30,7 +28,9 @@ const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\ const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config"; const APP_IDENTIFIER: &str = "br.com.esdrasrenan.sistemadechamados"; const MACHINE_STORE_FILENAME: &str = "machine-agent.json"; +#[allow(dead_code)] const ACL_FLAG_FILENAME: &str = "rustdesk_acl_unlocked.flag"; +#[allow(dead_code)] const RUSTDESK_ACL_STORE_KEY: &str = "rustdeskAclUnlockedAt"; const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password"; const SECURITY_APPROVE_MODE_VALUE: &str = "password"; @@ -85,11 +85,11 @@ fn define_custom_id_from_machine(exe_path: &Path, machine_id: Option<&str>) -> O }) { match set_custom_id(exe_path, value) { Ok(custom) => { - log_event(&format!("ID determinístico definido: {custom}")); + log_event(format!("ID determinístico definido: {custom}")); Some(custom) } Err(error) => { - log_event(&format!("Falha ao definir ID determinístico: {error}")); + log_event(format!("Falha ao definir ID determinístico: {error}")); None } } @@ -107,7 +107,7 @@ pub fn ensure_rustdesk( log_event("Iniciando preparo do RustDesk"); if let Err(error) = ensure_service_profiles_writable_preflight() { - log_event(&format!( + log_event(format!( "Aviso: não foi possível preparar ACL dos perfis do serviço ({error}). Continuando mesmo assim; o serviço pode não aplicar a senha." )); } @@ -116,7 +116,7 @@ pub fn ensure_rustdesk( // Isso preserva o ID quando o Raven é reinstalado mas o RustDesk permanece let preserved_remote_id = read_remote_id_from_profiles(); if let Some(ref id) = preserved_remote_id { - log_event(&format!("ID existente preservado antes da limpeza: {}", id)); + log_event(format!("ID existente preservado antes da limpeza: {}", id)); } let exe_path = detect_executable_path(); @@ -129,7 +129,7 @@ pub fn ensure_rustdesk( match stop_rustdesk_processes() { Ok(_) => log_event("Instâncias existentes do RustDesk encerradas"), - Err(error) => log_event(&format!( + Err(error) => log_event(format!( "Aviso: não foi possível parar completamente o RustDesk antes da reprovisionamento ({error})" )), } @@ -139,7 +139,7 @@ pub fn ensure_rustdesk( if freshly_installed { match purge_existing_rustdesk_profiles() { Ok(_) => log_event("Configurações antigas do RustDesk limpas (instalação fresca)"), - Err(error) => log_event(&format!( + Err(error) => log_event(format!( "Aviso: não foi possível limpar completamente os perfis existentes do RustDesk ({error})" )), } @@ -152,19 +152,19 @@ pub fn ensure_rustdesk( if trimmed.is_empty() { None } else { Some(trimmed) } }) { if let Err(error) = run_with_args(&exe_path, &["--config", value]) { - log_event(&format!("Falha ao aplicar configuração inline: {error}")); + log_event(format!("Falha ao aplicar configuração inline: {error}")); } else { log_event("Configuração aplicada via --config"); } } else { let config_path = write_config_files()?; - log_event(&format!( + log_event(format!( "Arquivo de configuração atualizado em {}", config_path.display() )); if let Err(error) = apply_config(&exe_path, &config_path) { - log_event(&format!("Falha ao aplicar configuração via CLI: {error}")); + log_event(format!("Falha ao aplicar configuração via CLI: {error}")); } else { log_event("Configuração aplicada via CLI"); } @@ -176,7 +176,7 @@ pub fn ensure_rustdesk( .unwrap_or_else(|| DEFAULT_PASSWORD.to_string()); if let Err(error) = set_password(&exe_path, &password) { - log_event(&format!("Falha ao definir senha padrão: {error}")); + log_event(format!("Falha ao definir senha padrão: {error}")); } else { log_event("Senha padrão definida com sucesso"); log_event("Aplicando senha nos perfis do RustDesk"); @@ -185,21 +185,21 @@ pub fn ensure_rustdesk( log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"); log_password_replication(&password); } - Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")), + Err(error) => log_event(format!("Falha ao persistir senha nos perfis: {error}")), } match propagate_password_profile() { Ok(_) => log_event("Perfil base propagado para ProgramData e perfis de serviço"), - Err(error) => log_event(&format!("Falha ao copiar perfil de senha: {error}")), + Err(error) => log_event(format!("Falha ao copiar perfil de senha: {error}")), } match replicate_password_artifacts() { Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"), - Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")), + Err(error) => log_event(format!("Falha ao replicar artefatos de senha: {error}")), } if let Err(error) = enforce_security_flags() { - log_event(&format!("Falha ao reforçar configuração de senha permanente: {error}")); + log_event(format!("Falha ao reforçar configuração de senha permanente: {error}")); } } @@ -207,7 +207,7 @@ pub fn ensure_rustdesk( // Isso garante que reinstalar o Raven nao muda o ID do RustDesk let custom_id = if let Some(ref existing_id) = preserved_remote_id { if !freshly_installed { - log_event(&format!("Reutilizando ID existente do RustDesk: {}", existing_id)); + log_event(format!("Reutilizando ID existente do RustDesk: {}", existing_id)); Some(existing_id.clone()) } else { // Instalacao fresca - define novo ID baseado no machine_id @@ -219,7 +219,7 @@ pub fn ensure_rustdesk( }; if let Err(error) = ensure_service_running(&exe_path) { - log_event(&format!("Falha ao reiniciar serviço do RustDesk: {error}")); + log_event(format!("Falha ao reiniciar serviço do RustDesk: {error}")); } else { log_event("Serviço RustDesk reiniciado/run ativo"); } @@ -227,10 +227,10 @@ pub fn ensure_rustdesk( let reported_id = match query_id_with_retries(&exe_path, 5) { Ok(value) => value, Err(error) => { - log_event(&format!("Falha ao obter ID após múltiplas tentativas: {error}")); + log_event(format!("Falha ao obter ID após múltiplas tentativas: {error}")); match read_remote_id_from_profiles().or_else(|| custom_id.clone()) { Some(value) => { - log_event(&format!("ID obtido via arquivos de perfil: {value}")); + log_event(format!("ID obtido via arquivos de perfil: {value}")); value } None => return Err(error), @@ -242,7 +242,7 @@ pub fn ensure_rustdesk( if let Some(expected) = custom_id.as_ref() { if expected != &reported_id { - log_event(&format!( + log_event(format!( "ID retornado difere do determinístico ({expected}) -> reaplicando ID determinístico" )); @@ -252,25 +252,25 @@ pub fn ensure_rustdesk( Ok(_) => match query_id_with_retries(&exe_path, 3) { Ok(rechecked) => { if &rechecked == expected { - log_event(&format!("ID determinístico aplicado com sucesso: {rechecked}")); + log_event(format!("ID determinístico aplicado com sucesso: {rechecked}")); final_id = rechecked; enforced = true; } else { - log_event(&format!( + log_event(format!( "ID ainda difere após reaplicação (esperado {expected}, reportado {rechecked}); usando ID reportado" )); final_id = rechecked; } } Err(error) => { - log_event(&format!( + log_event(format!( "Falha ao consultar ID após reaplicação: {error}; usando ID reportado ({reported_id})" )); final_id = reported_id.clone(); } }, Err(error) => { - log_event(&format!( + log_event(format!( "Falha ao reaplicar ID determinístico ({expected}): {error}; usando ID reportado ({reported_id})" )); final_id = reported_id.clone(); @@ -308,7 +308,7 @@ pub fn ensure_rustdesk( "lastError": serde_json::Value::Null }); if let Err(error) = upsert_machine_store_value("rustdesk", rustdesk_data) { - log_event(&format!("Aviso: falha ao salvar dados do RustDesk no store: {error}")); + log_event(format!("Aviso: falha ao salvar dados do RustDesk no store: {error}")); } else { log_event("Dados do RustDesk salvos no machine-agent.json"); } @@ -316,7 +316,7 @@ pub fn ensure_rustdesk( // Sincroniza com o backend imediatamente apos provisionar // O Rust faz o HTTP direto, sem passar pelo CSP do webview if let Err(error) = sync_remote_access_with_backend(&result) { - log_event(&format!("Aviso: falha ao sincronizar com backend: {error}")); + log_event(format!("Aviso: falha ao sincronizar com backend: {error}")); } else { log_event("Acesso remoto sincronizado com backend"); // Atualiza lastSyncedAt no store @@ -330,13 +330,13 @@ pub fn ensure_rustdesk( "lastError": serde_json::Value::Null }); if let Err(e) = upsert_machine_store_value("rustdesk", synced_data) { - log_event(&format!("Aviso: falha ao atualizar lastSyncedAt: {e}")); + log_event(format!("Aviso: falha ao atualizar lastSyncedAt: {e}")); } else { log_event("lastSyncedAt atualizado com sucesso"); } } - log_event(&format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version)); + log_event(format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version)); Ok(result) } @@ -403,7 +403,7 @@ fn write_config_files() -> Result { let config_contents = build_config_contents(); let main_path = program_data_config_dir().join("RustDesk2.toml"); write_file(&main_path, &config_contents)?; - log_event(&format!( + log_event(format!( "Config principal gravada em {}", main_path.display() )); @@ -412,7 +412,7 @@ fn write_config_files() -> Result { for service_dir in service_profile_dirs() { let service_profile = service_dir.join("RustDesk2.toml"); if let Err(error) = write_file(&service_profile, &config_contents) { - log_event(&format!( + log_event(format!( "Falha ao gravar config no perfil do serviço ({}): {error}", service_profile.display() )); @@ -421,7 +421,7 @@ fn write_config_files() -> Result { if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") { if let Err(error) = write_file(&appdata_path, &config_contents) { - log_event(&format!( + log_event(format!( "Falha ao atualizar config no AppData do usuário: {error}" )); } @@ -516,7 +516,7 @@ fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> { ensure_service_installed(exe_path)?; if let Err(error) = configure_service_startup() { - log_event(&format!( + log_event(format!( "Aviso: não foi possível reforçar autostart/recuperação do serviço RustDesk: {error}" )); } @@ -553,7 +553,7 @@ fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> { let _ = run_with_args(exe_path, &["--install-service"]); let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); if let Err(error) = start_sequence() { - log_event(&format!( + log_event(format!( "Falha ao subir o serviço RustDesk mesmo após reinstalação: {error}" )); } @@ -631,8 +631,8 @@ fn remove_rustdesk_autorun_artifacts() { for path in startup_paths { if path.exists() { match fs::remove_file(&path) { - Ok(_) => log_event(&format!("Atalho de inicialização do RustDesk removido: {}", path.display())), - Err(error) => log_event(&format!( + Ok(_) => log_event(format!("Atalho de inicialização do RustDesk removido: {}", path.display())), + Err(error) => log_event(format!( "Falha ao remover atalho de inicialização do RustDesk ({}): {}", path.display(), error @@ -650,7 +650,7 @@ fn remove_rustdesk_autorun_artifacts() { .status(); if let Ok(code) = status { if code.success() { - log_event(&format!("Entrada de auto-run RustDesk removida de {}", reg_path)); + log_event(format!("Entrada de auto-run RustDesk removida de {}", reg_path)); } } } @@ -658,7 +658,7 @@ fn remove_rustdesk_autorun_artifacts() { fn stop_rustdesk_processes() -> Result<(), RustdeskError> { if let Err(error) = try_stop_service() { - log_event(&format!( + log_event(format!( "Não foi possível parar o serviço RustDesk antes da sincronização: {error}" )); } @@ -774,12 +774,12 @@ fn ensure_remote_id_files(id: &str) { for dir in remote_id_directories() { let path = dir.join("RustDesk_local.toml"); match write_remote_id_value(&path, id) { - Ok(_) => log_event(&format!( + Ok(_) => log_event(format!( "remote_id atualizado para {} em {}", id, path.display() )), - Err(error) => log_event(&format!( + Err(error) => log_event(format!( "Falha ao atualizar remote_id em {}: {error}", path.display() )), @@ -821,7 +821,7 @@ fn ensure_password_files(secret: &str) -> Result<(), String> { if let Err(error) = write_toml_kv(&password_path, "password", secret) { errors.push(format!("{} -> {}", password_path.display(), error)); } else { - log_event(&format!( + log_event(format!( "Senha escrita via fallback em {}", password_path.display() )); @@ -829,12 +829,12 @@ fn ensure_password_files(secret: &str) -> Result<(), String> { let local_path = dir.join("RustDesk_local.toml"); if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) { - log_event(&format!( + log_event(format!( "Falha ao ajustar verification-method em {}: {error}", local_path.display() )); } else { - log_event(&format!( + log_event(format!( "verification-method atualizado para {} em {}", SECURITY_VERIFICATION_VALUE, local_path.display() @@ -843,19 +843,19 @@ fn ensure_password_files(secret: &str) -> Result<(), String> { let rustdesk2_path = dir.join("RustDesk2.toml"); if let Err(error) = enforce_security_in_rustdesk2(&rustdesk2_path) { - log_event(&format!( + log_event(format!( "Falha ao ajustar flags no RustDesk2.toml em {}: {error}", rustdesk2_path.display() )); } if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) { - log_event(&format!( + log_event(format!( "Falha ao ajustar approve-mode em {}: {error}", local_path.display() )); } else { - log_event(&format!( + log_event(format!( "approve-mode atualizado para {} em {}", SECURITY_APPROVE_MODE_VALUE, local_path.display() @@ -877,7 +877,7 @@ fn enforce_security_flags() -> Result<(), String> { if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) { errors.push(format!("{} -> {}", local_path.display(), error)); } else { - log_event(&format!( + log_event(format!( "verification-method atualizado para {} em {}", SECURITY_VERIFICATION_VALUE, local_path.display() @@ -887,7 +887,7 @@ fn enforce_security_flags() -> Result<(), String> { if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) { errors.push(format!("{} -> {}", local_path.display(), error)); } else { - log_event(&format!( + log_event(format!( "approve-mode atualizado para {} em {}", SECURITY_APPROVE_MODE_VALUE, local_path.display() @@ -921,7 +921,7 @@ fn propagate_password_profile() -> io::Result { if !src_path.exists() { continue; } - log_event(&format!( + log_event(format!( "Copiando {} para ProgramData/serviços", src_path.display() )); @@ -929,7 +929,7 @@ fn propagate_password_profile() -> io::Result { for dest_root in propagation_destinations() { let target_path = dest_root.join(filename); copy_overwrite(&src_path, &target_path)?; - log_event(&format!( + log_event(format!( "{} propagado para {}", filename, target_path.display() @@ -969,7 +969,7 @@ fn replicate_password_artifacts() -> io::Result<()> { let target_path = dest.join(name); copy_overwrite(&source_path, &target_path)?; - log_event(&format!( + log_event(format!( "Artefato de senha {name} replicado para {}", target_path.display() )); @@ -981,13 +981,11 @@ fn replicate_password_artifacts() -> io::Result<()> { fn purge_existing_rustdesk_profiles() -> Result<(), String> { let mut errors = Vec::new(); - let mut cleaned_any = false; for dir in remote_id_directories() { match purge_config_dir(&dir) { Ok(true) => { - cleaned_any = true; - log_event(&format!( + log_event(format!( "Perfis antigos removidos em {}", dir.display() )); @@ -997,9 +995,7 @@ fn purge_existing_rustdesk_profiles() -> Result<(), String> { } } - if cleaned_any { - Ok(()) - } else if errors.is_empty() { + if errors.is_empty() { Ok(()) } else { Err(errors.join(" | ")) @@ -1030,6 +1026,7 @@ fn purge_config_dir(dir: &Path) -> Result { Ok(removed) } +#[allow(dead_code)] fn run_powershell_elevated(script: &str) -> Result<(), String> { let temp_dir = env::temp_dir(); let payload = temp_dir.join("raven_payload.ps1"); @@ -1077,6 +1074,7 @@ exit $process.ExitCode Err(format!("elevated ps exit {:?}", status.code())) } +#[allow(dead_code)] fn fix_profile_acl(target: &Path) -> Result<(), String> { let target_str = target.display().to_string(); let transcript = env::temp_dir().join("raven_acl_ps.log"); @@ -1111,7 +1109,7 @@ try {{ let result = run_powershell_elevated(&script); if result.is_err() { if let Ok(content) = fs::read_to_string(&transcript) { - log_event(&format!( + log_event(format!( "ACL transcript para {}:\n{}", target.display(), content )); @@ -1122,6 +1120,9 @@ try {{ } fn ensure_service_profiles_writable_preflight() -> Result<(), String> { + // Verificamos se os diretorios de perfil sao graváveis + // Se nao forem, apenas logamos aviso - o Raven Service deve lidar com isso + // Nao usamos elevacao para evitar UAC adicional let mut blocked_dirs = Vec::new(); for dir in service_profile_dirs() { if !can_write_dir(&dir) { @@ -1133,53 +1134,46 @@ fn ensure_service_profiles_writable_preflight() -> Result<(), String> { return Ok(()); } - if has_acl_unlock_flag() { - log_event("Perfis do serviço voltaram a bloquear escrita; reaplicando correção de ACL"); - } else { - log_event("Executando ajuste inicial de ACL dos perfis do serviço (requer UAC)"); - } + // Apenas logamos aviso - o serviço RavenService deve lidar com permissões + log_event(format!( + "Aviso: alguns perfis de serviço não são graváveis: {:?}. O Raven Service deve configurar permissões.", + blocked_dirs.iter().map(|d| d.display().to_string()).collect::>() + )); - let mut last_error: Option = None; - for dir in blocked_dirs.iter() { - log_event(&format!( - "Tentando corrigir ACL via UAC (preflight) em {}...", - dir.display() - )); - if let Err(error) = fix_profile_acl(dir) { - last_error = Some(error); - continue; - } - if can_write_dir(dir) { - log_event(&format!( - "ACL ajustada com sucesso em {}", - dir.display() - )); - } else { - last_error = Some(format!( - "continua sem permissão para {} mesmo após preflight", - dir.display() - )); - } - } - - if blocked_dirs.iter().all(|dir| can_write_dir(dir)) { - mark_acl_unlock_flag(); - Ok(()) - } else { - Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into())) - } + // Retornamos Ok para não bloquear o fluxo + // O Raven Service, rodando como LocalSystem, pode gravar nesses diretórios + Ok(()) } fn stop_service_elevated() -> Result<(), String> { - let script = r#" -$ErrorActionPreference='Stop' -$service = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue -if ($service -and $service.Status -ne 'Stopped') { - Stop-Service -Name 'RustDesk' -Force -ErrorAction Stop - $service.WaitForStatus('Stopped','00:00:10') -} -"#; - run_powershell_elevated(script) + // Tentamos parar o serviço RustDesk sem elevação + // Se falhar, apenas logamos aviso - o Raven Service pode lidar com isso + // Não usamos elevação para evitar UAC adicional + let output = Command::new("sc") + .args(["stop", "RustDesk"]) + .output(); + + match output { + Ok(result) => { + if result.status.success() { + // Aguarda um pouco para o serviço parar + std::thread::sleep(std::time::Duration::from_secs(2)); + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&result.stderr); + log_event(format!( + "Aviso: não foi possível parar o serviço RustDesk sem elevação: {}", + stderr.trim() + )); + // Retornamos Ok para não bloquear - o serviço pode estar já parado + Ok(()) + } + } + Err(e) => { + log_event(format!("Aviso: falha ao executar sc stop RustDesk: {e}")); + Ok(()) + } + } } fn can_write_dir(dir: &Path) -> bool { @@ -1339,21 +1333,21 @@ fn log_password_replication(secret: &str) { fn log_password_match(path: &Path, secret: &str) { match read_password_from_file(path) { Some(value) if value == secret => { - log_event(&format!( + log_event(format!( "Senha confirmada em {} ({})", path.display(), mask_secret(&value) )); } Some(value) => { - log_event(&format!( + log_event(format!( "Aviso: senha divergente ({}) em {}", mask_secret(&value), path.display() )); } None => { - log_event(&format!( + log_event(format!( "Aviso: chave 'password' não encontrada em {}", path.display() )); @@ -1469,21 +1463,24 @@ fn write_machine_store_object(map: JsonMap) -> Result<(), Str } fn upsert_machine_store_value(key: &str, value: JsonValue) -> Result<(), String> { - let mut map = read_machine_store_object().unwrap_or_else(JsonMap::new); + let mut map = read_machine_store_object().unwrap_or_default(); map.insert(key.to_string(), value); write_machine_store_object(map) } +#[allow(dead_code)] fn machine_store_key_exists(key: &str) -> bool { read_machine_store_object() .map(|map| map.contains_key(key)) .unwrap_or(false) } +#[allow(dead_code)] fn acl_flag_file_path() -> Option { raven_appdata_root().map(|dir| dir.join(ACL_FLAG_FILENAME)) } +#[allow(dead_code)] fn has_acl_unlock_flag() -> bool { if let Some(flag) = acl_flag_file_path() { if flag.exists() { @@ -1493,6 +1490,7 @@ fn has_acl_unlock_flag() -> bool { machine_store_key_exists(RUSTDESK_ACL_STORE_KEY) } +#[allow(dead_code)] fn mark_acl_unlock_flag() { let timestamp = Utc::now().timestamp_millis(); if let Some(flag_path) = acl_flag_file_path() { @@ -1500,7 +1498,7 @@ fn mark_acl_unlock_flag() { let _ = fs::create_dir_all(parent); } if let Err(error) = fs::write(&flag_path, timestamp.to_string()) { - log_event(&format!( + log_event(format!( "Falha ao gravar flag de ACL em {}: {error}", flag_path.display() )); @@ -1508,7 +1506,7 @@ fn mark_acl_unlock_flag() { } if let Err(error) = upsert_machine_store_value(RUSTDESK_ACL_STORE_KEY, JsonValue::from(timestamp)) { - log_event(&format!( + log_event(format!( "Falha ao registrar flag de ACL no machine-agent: {error}" )); } @@ -1547,7 +1545,7 @@ fn sync_remote_access_with_backend(result: &crate::RustdeskProvisioningResult) - .and_then(|v| v.as_str()) .unwrap_or("https://tickets.esdrasrenan.com.br"); - log_event(&format!("Sincronizando com backend: {} (machineId: {})", api_base_url, machine_id)); + log_event(format!("Sincronizando com backend: {} (machineId: {})", api_base_url, machine_id)); // Monta payload conforme schema esperado pelo backend // Schema: { machineToken, provider, identifier, password?, url?, username?, notes? } @@ -1575,13 +1573,13 @@ fn sync_remote_access_with_backend(result: &crate::RustdeskProvisioningResult) - .send()?; if response.status().is_success() { - log_event(&format!("Sync com backend OK: status {}", response.status())); + log_event(format!("Sync com backend OK: status {}", response.status())); Ok(()) } else { let status = response.status(); let body = response.text().unwrap_or_default(); let body_preview = if body.len() > 200 { &body[..200] } else { &body }; - log_event(&format!("Sync com backend falhou: {} - {}", status, body_preview)); + log_event(format!("Sync com backend falhou: {} - {}", status, body_preview)); Err(RustdeskError::CommandFailed { command: "sync_remote_access".to_string(), status: Some(status.as_u16() as i32) diff --git a/apps/desktop/src-tauri/src/service_client.rs b/apps/desktop/src-tauri/src/service_client.rs new file mode 100644 index 0000000..f2af2ed --- /dev/null +++ b/apps/desktop/src-tauri/src/service_client.rs @@ -0,0 +1,244 @@ +//! Cliente IPC para comunicacao com o Raven Service +//! +//! Este modulo permite que o app Tauri se comunique com o Raven Service +//! via Named Pipes para executar operacoes privilegiadas. + +#![allow(dead_code)] + +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader, Write}; +use std::time::Duration; +use thiserror::Error; + +const PIPE_NAME: &str = r"\\.\pipe\RavenService"; + +#[derive(Debug, Error)] +pub enum ServiceClientError { + #[error("Servico nao disponivel: {0}")] + ServiceUnavailable(String), + + #[error("Erro de comunicacao: {0}")] + CommunicationError(String), + + #[error("Erro de serializacao: {0}")] + SerializationError(#[from] serde_json::Error), + + #[error("Erro do servico: {message} (code: {code})")] + ServiceError { code: i32, message: String }, + + #[error("Timeout aguardando resposta")] + Timeout, +} + +#[derive(Debug, Serialize)] +struct Request { + id: String, + method: String, + params: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +struct Response { + id: String, + result: Option, + error: Option, +} + +#[derive(Debug, Deserialize)] +struct ErrorResponse { + code: i32, + message: String, +} + +// ============================================================================= +// Tipos de Resultado +// ============================================================================= + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UsbPolicyResult { + pub success: bool, + pub policy: String, + pub error: Option, + pub applied_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskResult { + pub id: String, + pub password: String, + pub installed_version: Option, + pub updated: bool, + pub last_provisioned_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RustdeskStatus { + pub installed: bool, + pub running: bool, + pub id: Option, + pub version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HealthCheckResult { + pub status: String, + pub service: String, + pub version: String, + pub timestamp: i64, +} + +// ============================================================================= +// Cliente +// ============================================================================= + +/// Verifica se o servico esta disponivel +pub fn is_service_available() -> bool { + health_check().is_ok() +} + +/// Verifica saude do servico +pub fn health_check() -> Result { + let response = call_service("health_check", serde_json::json!({}))?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +/// Aplica politica de USB +pub fn apply_usb_policy(policy: &str) -> Result { + let response = call_service( + "apply_usb_policy", + serde_json::json!({ "policy": policy }), + )?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +/// Obtem politica de USB atual +pub fn get_usb_policy() -> Result { + let response = call_service("get_usb_policy", serde_json::json!({}))?; + response + .get("policy") + .and_then(|p| p.as_str()) + .map(String::from) + .ok_or_else(|| ServiceClientError::CommunicationError("Resposta invalida".into())) +} + +/// Provisiona RustDesk +pub fn provision_rustdesk( + config: Option<&str>, + password: Option<&str>, + machine_id: Option<&str>, +) -> Result { + let params = serde_json::json!({ + "config": config, + "password": password, + "machineId": machine_id, + }); + let response = call_service("provision_rustdesk", params)?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +/// Obtem status do RustDesk +pub fn get_rustdesk_status() -> Result { + let response = call_service("get_rustdesk_status", serde_json::json!({}))?; + serde_json::from_value(response).map_err(|e| e.into()) +} + +// ============================================================================= +// Comunicacao IPC +// ============================================================================= + +fn call_service( + method: &str, + params: serde_json::Value, +) -> Result { + // Gera ID unico para a requisicao + let id = uuid::Uuid::new_v4().to_string(); + + let request = Request { + id: id.clone(), + method: method.to_string(), + params, + }; + + // Serializa requisicao + let request_json = serde_json::to_string(&request)?; + + // Conecta ao pipe + let mut pipe = connect_to_pipe()?; + + // Envia requisicao + writeln!(pipe, "{}", request_json).map_err(|e| { + ServiceClientError::CommunicationError(format!("Erro ao enviar requisicao: {}", e)) + })?; + pipe.flush().map_err(|e| { + ServiceClientError::CommunicationError(format!("Erro ao flush: {}", e)) + })?; + + // Le resposta + let mut reader = BufReader::new(pipe); + let mut response_line = String::new(); + + reader.read_line(&mut response_line).map_err(|e| { + ServiceClientError::CommunicationError(format!("Erro ao ler resposta: {}", e)) + })?; + + // Parse da resposta + let response: Response = serde_json::from_str(&response_line)?; + + // Verifica se o ID bate + if response.id != id { + return Err(ServiceClientError::CommunicationError( + "ID de resposta nao corresponde".into(), + )); + } + + // Verifica erro + if let Some(error) = response.error { + return Err(ServiceClientError::ServiceError { + code: error.code, + message: error.message, + }); + } + + // Retorna resultado + response + .result + .ok_or_else(|| ServiceClientError::CommunicationError("Resposta sem resultado".into())) +} + +#[cfg(target_os = "windows")] +fn connect_to_pipe() -> Result { + // Tenta conectar ao pipe com retry + let mut attempts = 0; + let max_attempts = 3; + + loop { + match std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(PIPE_NAME) + { + Ok(file) => return Ok(file), + Err(e) => { + attempts += 1; + if attempts >= max_attempts { + return Err(ServiceClientError::ServiceUnavailable(format!( + "Nao foi possivel conectar ao servico apos {} tentativas: {}", + max_attempts, e + ))); + } + std::thread::sleep(Duration::from_millis(500)); + } + } + } +} + +#[cfg(not(target_os = "windows"))] +fn connect_to_pipe() -> Result { + Err(ServiceClientError::ServiceUnavailable( + "Named Pipes so estao disponiveis no Windows".into(), + )) +} diff --git a/apps/desktop/src-tauri/src/usb_control.rs b/apps/desktop/src-tauri/src/usb_control.rs index 24eebfa..a95e0a5 100644 --- a/apps/desktop/src-tauri/src/usb_control.rs +++ b/apps/desktop/src-tauri/src/usb_control.rs @@ -93,22 +93,10 @@ mod windows_impl { applied_at: Some(now), }), Err(err) => { - // Tenta elevação se faltou permissão + // Se faltou permissão, retorna erro - o serviço deve ser usado + // Não fazemos elevação aqui para evitar UAC adicional if is_permission_error(&err) { - if let Err(elevated_err) = apply_policy_with_elevation(policy) { - return Err(elevated_err); - } - // Revalida a policy após elevação - let current = get_current_policy()?; - if current != policy { - return Err(UsbControlError::PermissionDenied); - } - return Ok(UsbPolicyResult { - success: true, - policy: policy.as_str().to_string(), - error: None, - applied_at: Some(now), - }); + return Err(UsbControlError::PermissionDenied); } Err(err) } @@ -219,10 +207,8 @@ mod windows_impl { key.set_value("WriteProtect", &1u32) .map_err(map_winreg_error)?; - } else { - if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) { - let _ = key.set_value("WriteProtect", &0u32); - } + } else if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) { + let _ = key.set_value("WriteProtect", &0u32); } Ok(()) @@ -269,6 +255,7 @@ mod windows_impl { } } + #[allow(dead_code)] fn apply_policy_with_elevation(policy: UsbPolicy) -> Result<(), UsbControlError> { // Cria script temporário para aplicar as chaves via PowerShell elevado let temp_dir = std::env::temp_dir(); @@ -321,7 +308,7 @@ try {{ policy = policy_str ); - fs::write(&script_path, script).map_err(|e| UsbControlError::Io(e))?; + fs::write(&script_path, script).map_err(UsbControlError::Io)?; // Start-Process com RunAs para acionar UAC let arg = format!( @@ -333,7 +320,7 @@ try {{ .arg("-Command") .arg(arg) .status() - .map_err(|e| UsbControlError::Io(e))?; + .map_err(UsbControlError::Io)?; if !status.success() { return Err(UsbControlError::PermissionDenied); @@ -362,7 +349,7 @@ try {{ .args(["/target:computer", "/force"]) .creation_flags(CREATE_NO_WINDOW) .output() - .map_err(|e| UsbControlError::Io(e))?; + .map_err(UsbControlError::Io)?; if !output.status.success() { // Nao e critico se falhar, apenas log diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index d7672b5..b9a94d1 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -50,6 +50,9 @@ "icons/icon.png", "icons/Raven.png" ], + "resources": { + "../service/target/release/raven-service.exe": "raven-service.exe" + }, "windows": { "webviewInstallMode": { "type": "skip"