diff --git a/.cargo/config.toml b/.cargo/config.toml index 16ae724..b2537fc 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -39,3 +39,6 @@ codegen-units = 1 panic = "abort" debug = false +[env] +CC_armv7_unknown_linux_musleabihf = "clang" +AR_armv7_unknown_linux_musleabihf = "llvm-ar" diff --git a/Cargo.lock b/Cargo.lock index 1680d4e..4678680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,8 +1091,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1102,9 +1104,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1248,18 +1252,39 @@ dependencies = [ ] [[package]] -name = "hyper-util" -version = "0.1.11" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1543,6 +1568,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -1717,6 +1752,12 @@ dependencies = [ "imgref", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mach2" version = "0.4.2" @@ -2251,6 +2292,61 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2419,6 +2515,7 @@ dependencies = [ "log", "mime_guess", "rayhunter", + "reqwest", "serde", "serde_json", "simple_logger", @@ -2490,38 +2587,40 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64", "bytes", "futures-core", - "futures-util", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "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", - "windows-registry", + "webpki-roots", ] [[package]] @@ -2530,6 +2629,20 @@ version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +[[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 = "rootshell" version = "0.4.0" @@ -2573,6 +2686,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -2599,15 +2718,41 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -3032,6 +3177,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +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.45.0" @@ -3081,6 +3241,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3171,6 +3341,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3227,6 +3415,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[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.4" @@ -3384,6 +3578,25 @@ dependencies = [ "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.8" @@ -3437,7 +3650,7 @@ dependencies = [ "windows-interface 0.59.1", "windows-link", "windows-result 0.3.3", - "windows-strings 0.4.1", + "windows-strings", ] [[package]] @@ -3490,17 +3703,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.3", - "windows-strings 0.3.1", - "windows-targets 0.53.0", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -3519,15 +3721,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-strings" version = "0.4.1" @@ -3588,29 +3781,13 @@ 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_gnullvm", "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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3623,12 +3800,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3641,12 +3812,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3659,24 +3824,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3689,12 +3842,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3707,12 +3854,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3725,12 +3866,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3743,12 +3878,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" version = "0.7.10" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 1b72da6..62fee66 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -23,8 +23,17 @@ path = "src/check.rs" rayhunter = { path = "../lib" } toml = "0.8.8" serde = { version = "1.0.193", features = ["derive"] } -tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] } -axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] } +tokio = { version = "1.44.2", default-features = false, features = [ + "fs", + "signal", + "process", + "rt-multi-thread", +] } +axum = { version = "0.8", default-features = false, features = [ + "http1", + "tokio", + "json", +] } thiserror = "1.0.52" libc = "0.2.150" log = "0.4.20" @@ -38,8 +47,14 @@ tokio-stream = { version = "0.1.14", default-features = false } futures = { version = "0.3.30", default-features = false } clap = { version = "4.5.2", features = ["derive"] } serde_json = "1.0.114" -image = { version = "0.25.1", default-features = false, features = ["png", "gif"] } +image = { version = "0.25.1", default-features = false, features = [ + "png", + "gif", +] } tempfile = "3.10.1" simple_logger = "5.0.0" async_zip = { version = "0.0.17", features = ["tokio"] } anyhow = "1.0.98" +reqwest = { version = "0.12.20", default-features = false, features = [ + "rustls-tls", +] } diff --git a/bin/src/config.rs b/bin/src/config.rs index 5d3ffec..5745ddf 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -14,6 +14,7 @@ pub struct Config { pub enable_dummy_analyzer: bool, pub colorblind_mode: bool, pub key_input_mode: u8, + pub ntfy_topic: Option, pub analyzers: AnalyzerConfig, } @@ -28,6 +29,7 @@ impl Default for Config { colorblind_mode: false, key_input_mode: 0, analyzers: AnalyzerConfig::default(), + ntfy_topic: None, } } } diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 05d3bd0..cf40f84 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -5,6 +5,7 @@ mod display; mod dummy_analyzer; mod error; mod key_input; +mod notifications; mod pcap; mod qmdl_store; mod server; @@ -17,6 +18,7 @@ use std::sync::Arc; use crate::config::{parse_args, parse_config}; use crate::diag::run_diag_read_thread; use crate::error::RayhunterError; +use crate::notifications::{run_notification_worker, NotificationService}; use crate::pcap::get_pcap; use crate::qmdl_store::RecordingStore; use crate::server::{get_config, get_qmdl, get_zip, serve_static, set_config, ServerState}; @@ -212,6 +214,9 @@ async fn run_with_config( let (analysis_tx, analysis_rx) = mpsc::channel::(5); let mut maybe_ui_shutdown_tx = None; let mut maybe_key_input_shutdown_tx = None; + + let notification_service = NotificationService::new(config.ntfy_topic.clone()); + if !config.debug_mode { let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel(); maybe_ui_shutdown_tx = Some(ui_shutdown_tx); @@ -232,6 +237,7 @@ async fn run_with_config( analysis_tx.clone(), config.enable_dummy_analyzer, config.analyzers.clone(), + notification_service.new_handler(), ); info!("Starting UI"); display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); @@ -271,6 +277,7 @@ async fn run_with_config( qmdl_store_lock.clone(), analysis_tx.clone(), ); + run_notification_worker(&task_tracker, notification_service); let state = Arc::new(ServerState { config_path: args.config_path.clone(), config, diff --git a/bin/src/diag.rs b/bin/src/diag.rs index bf35e6e..6246502 100644 --- a/bin/src/diag.rs +++ b/bin/src/diag.rs @@ -1,5 +1,6 @@ use std::pin::pin; use std::sync::Arc; +use std::time::Duration; use axum::body::Body; use axum::extract::{Path, State}; @@ -20,6 +21,7 @@ use tokio_util::task::TaskTracker; use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter}; use crate::display; +use crate::notifications::Notification; use crate::qmdl_store::{RecordingStore, RecordingStoreError}; use crate::server::ServerState; @@ -38,6 +40,7 @@ pub fn run_diag_read_thread( analysis_sender: Sender, enable_dummy_analyzer: bool, analyzer_config: AnalyzerConfig, + notification_channel: tokio::sync::mpsc::Sender, ) { task_tracker.spawn(async move { let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry"); @@ -45,6 +48,7 @@ pub fn run_diag_read_thread( let mut diag_stream = pin!(dev.as_stream().into_stream()); let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await .expect("failed to create analysis writer")); + loop { tokio::select! { msg = qmdl_file_rx.recv() => { @@ -130,13 +134,18 @@ pub fn run_diag_read_thread( } if let Some(analysis_writer) = maybe_analysis_writer.as_mut() { - let analysis_output = analysis_writer.analyze(container).await + let (analysis_file_len, heuristic_warning) = analysis_writer.analyze(container).await .expect("failed to analyze container"); - let (analysis_file_len, heuristic_warning) = analysis_output; if heuristic_warning { info!("a heuristic triggered on this run!"); ui_update_sender.send(display::DisplayState::WarningDetected).await .expect("couldn't send ui update message: {}"); + notification_channel.send( + Notification::new( + "heuristic-warning".to_string(), + "New warning triggered!".to_string(), + Some(Duration::from_secs(60*5))) + ).await.expect("Failed to send to notification channel"); } let mut qmdl_store = qmdl_store_lock.write().await; let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???"); diff --git a/bin/src/notifications.rs b/bin/src/notifications.rs new file mode 100644 index 0000000..7ed92b3 --- /dev/null +++ b/bin/src/notifications.rs @@ -0,0 +1,152 @@ +use std::{ + cmp::min, + collections::HashMap, + time::{Duration, Instant}, +}; + +use log::error; +use tokio::sync::mpsc::{self, error::TryRecvError}; +use tokio_util::task::TaskTracker; + +static NTFY_BASE_URL: &str = "https://ntfy.sh/"; + +pub struct Notification { + message_type: String, + message: String, + debounce: Option, +} + +impl Notification { + pub fn new(message_type: String, message: String, debounce: Option) -> Self { + Notification { + message_type, + message, + debounce, + } + } +} + +struct NotificationStatus { + message: String, + needs_sending: bool, + last_sent: Option, + last_attempt: Option, + failed_since_last_success: u32, +} + +pub struct NotificationService { + channel_name: Option, + tx: mpsc::Sender, + rx: mpsc::Receiver, +} + +impl NotificationService { + pub fn new(channel_name: Option) -> Self { + let (tx, rx) = mpsc::channel(10); + Self { + channel_name, + tx, + rx, + } + } + + pub fn new_handler(&self) -> mpsc::Sender { + return self.tx.clone(); + } +} + +pub fn run_notification_worker( + task_tracker: &TaskTracker, + mut notification_service: NotificationService, +) { + task_tracker.spawn(async move { + let channel_name = notification_service.channel_name.unwrap_or("".into()); + + if channel_name != String::from("") { + let mut notification_statuses = HashMap::new(); + let http_client = reqwest::Client::new(); + + loop { + // Get any notifications since the last time we checked + loop { + match notification_service.rx.try_recv() { + Ok(notification) => { + let status = notification_statuses + .entry(notification.message_type) + .or_insert_with(|| NotificationStatus { + message: "".to_string(), + needs_sending: true, + last_sent: None, + last_attempt: None, + failed_since_last_success: 0, + }); + // Ignore if we're in the debounce period + if let Some(debounce) = notification.debounce { + if let Some(last_sent) = status.last_sent { + if last_sent.elapsed() < debounce { + continue; + } + } + } + status.message = notification.message; + status.needs_sending = true; + } + Err(TryRecvError::Empty) => { + break; + } + Err(TryRecvError::Disconnected) => { + return; + } + } + } + + // Attempt to send pending notifications + for notification in notification_statuses.values_mut() { + if !notification.needs_sending { + continue; + } + + // Backoff retries, up to a maximum of 256 seconds. + if let Some(last_attempt) = notification.last_attempt { + let min_wait_time = Duration::from_secs( + 2u64.pow(min(notification.failed_since_last_success, 8)), + ); + if last_attempt.elapsed() < min_wait_time { + continue; + } + } + + match http_client + .post(format!("{}{}", NTFY_BASE_URL, channel_name)) + .body(notification.message.clone()) + .send() + .await + { + Ok(response) => { + if response.status().is_success() { + notification.last_sent = Some(Instant::now()); + notification.failed_since_last_success = 0; + notification.needs_sending = false; + } else { + notification.failed_since_last_success += 1; + notification.last_attempt = Some(Instant::now()); + } + } + Err(e) => { + error!("Failed to send notification to ntfy: {}", e); + notification.failed_since_last_success += 1; + } + } + } + + tokio::time::sleep(Duration::from_secs(2)).await; + } + } + // If there's no channel name we'll just discard the notifications + else { + loop { + notification_service.rx.recv().await; + } + } + }); +} diff --git a/bin/web/src/lib/components/ConfigForm.svelte b/bin/web/src/lib/components/ConfigForm.svelte index 4c7819e..f5b91a6 100644 --- a/bin/web/src/lib/components/ConfigForm.svelte +++ b/bin/web/src/lib/components/ConfigForm.svelte @@ -94,6 +94,17 @@ +
+ + +
+