diff --git a/Cargo.lock b/Cargo.lock index 45123faa..7de49121 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -193,6 +193,26 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "arboard" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "once_cell", + "parking_lot 0.12.1", + "thiserror", + "winapi 0.3.9", + "x11rb", +] + [[package]] name = "argon2" version = "0.5.0" @@ -727,6 +747,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.9.0" @@ -827,6 +853,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "byteorder" version = "1.4.3" @@ -1081,6 +1113,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi 0.3.9", +] + [[package]] name = "cmake" version = "0.1.50" @@ -1103,6 +1146,12 @@ dependencies = [ "owo-colors", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.6" @@ -1241,6 +1290,30 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.3", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.3", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.8" @@ -1413,8 +1486,6 @@ dependencies = [ "lazy_static", "libc", "log", - "maplit", - "ncurses", "signal-hook", "tokio 1.28.2", "unicode-segmentation", @@ -1431,7 +1502,7 @@ dependencies = [ "flexi_logger", "lazy_static", "log", - "time 0.3.22", + "time 0.3.9", "unicode-width", ] @@ -1473,7 +1544,7 @@ dependencies = [ "owning_ref", "serde_json", "serde_yaml", - "time 0.3.22", + "time 0.3.9", "tokio 1.28.2", "toml 0.7.4", "unicode-segmentation", @@ -1826,6 +1897,16 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "ethbloom" version = "0.13.0" @@ -1912,6 +1993,15 @@ dependencies = [ "instant", ] +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ffi-support" version = "0.4.4" @@ -1965,7 +2055,7 @@ dependencies = [ "regex", "rustversion", "thiserror", - "time 0.3.22", + "time 0.3.9", ] [[package]] @@ -1987,6 +2077,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -2176,6 +2281,16 @@ dependencies = [ "version_check 0.9.4", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2593,6 +2708,21 @@ dependencies = [ "xmltree", ] +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", + "tiff", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -2778,6 +2908,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.64" @@ -3029,6 +3165,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "maplit" version = "1.0.2" @@ -3129,6 +3274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -3232,17 +3378,6 @@ dependencies = [ "socket2 0.4.9", ] -[[package]] -name = "ncurses" -version = "5.101.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "ndk" version = "0.7.0" @@ -3402,6 +3537,18 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.26.2" @@ -3590,6 +3737,35 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.30.4" @@ -4044,6 +4220,19 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.1", +] + [[package]] name = "polling" version = "2.8.0" @@ -5091,6 +5280,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + [[package]] name = "simdutf8" version = "0.1.4" @@ -5116,7 +5311,7 @@ checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" dependencies = [ "log", "termcolor", - "time 0.3.22", + "time 0.3.9", ] [[package]] @@ -5261,6 +5456,12 @@ dependencies = [ "pin-project-lite 0.2.9", ] +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strsim" version = "0.8.0" @@ -5422,6 +5623,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.1.45" @@ -5450,24 +5662,16 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ "itoa", "libc", "num_threads", - "serde", - "time-core", - "time-macros 0.2.9", + "time-macros 0.2.4", ] -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - [[package]] name = "time-macros" version = "0.1.1" @@ -5480,12 +5684,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" -dependencies = [ - "time-core", -] +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "time-macros-impl" @@ -5797,7 +5998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ "crossbeam-channel", - "time 0.3.22", + "time 0.3.9", "tracing-subscriber", ] @@ -6164,6 +6365,7 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" name = "veilid-cli" version = "0.1.0" dependencies = [ + "arboard", "async-std", "async-tungstenite 0.8.0", "bugsalot", @@ -6683,6 +6885,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "wg" version = "0.3.2" @@ -6754,6 +6962,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -7022,6 +7239,28 @@ dependencies = [ "tap", ] +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname", + "nix 0.24.3", + "winapi 0.3.9", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", +] + [[package]] name = "x25519-dalek" version = "1.2.0" diff --git a/external/cursive-flexi-logger-view b/external/cursive-flexi-logger-view index effa60ce..1eaa0814 160000 --- a/external/cursive-flexi-logger-view +++ b/external/cursive-flexi-logger-view @@ -1 +1 @@ -Subproject commit effa60cea24e99f294865ed325ffc57612d72785 +Subproject commit 1eaa0814e0dd7e585b50c5d4e6e7cb9474d7a758 diff --git a/veilid-cli/Cargo.toml b/veilid-cli/Cargo.toml index 1701aa1e..b2d2d5dd 100644 --- a/veilid-cli/Cargo.toml +++ b/veilid-cli/Cargo.toml @@ -11,7 +11,6 @@ path = "src/main.rs" [features] default = [ "rt-tokio" ] -macos = [ "cursive/ncurses-backend" ] rt-async-std = [ "async-std", "veilid-tools/rt-async-std", "cursive/rt-async-std" ] rt-tokio = [ "tokio", "tokio-util", "veilid-tools/rt-tokio", "cursive/rt-tokio" ] @@ -26,6 +25,7 @@ cursive_buffered_backend = { path = "../external/cursive_buffered_backend" } # cursive-multiplex = "0.6.0" # cursive_tree_view = "0.6.0" cursive_table_view = "0.14.0" +arboard = "3.2.0" # cursive-tabs = "0.5.0" clap = "^3" directories = "^4" diff --git a/veilid-cli/src/client_api_connection.rs b/veilid-cli/src/client_api_connection.rs index e790dcd9..0f5e20b2 100644 --- a/veilid-cli/src/client_api_connection.rs +++ b/veilid-cli/src/client_api_connection.rs @@ -83,7 +83,7 @@ impl ClientApiConnection { async fn process_veilid_update(&self, update: json::JsonValue) { let comproc = self.inner.lock().comproc.clone(); let Some(kind) = update["kind"].as_str() else { - comproc.log_message(format!("missing update kind: {}", update)); + comproc.log_message(Level::Error, format!("missing update kind: {}", update)); return; }; match kind { @@ -113,7 +113,7 @@ impl ClientApiConnection { comproc.update_value_change(&update); } _ => { - comproc.log_message(format!("unknown update kind: {}", update)); + comproc.log_message(Level::Error, format!("unknown update kind: {}", update)); } } } diff --git a/veilid-cli/src/command_processor.rs b/veilid-cli/src/command_processor.rs index dcf0a8cb..5bd05bd7 100644 --- a/veilid-cli/src/command_processor.rs +++ b/veilid-cli/src/command_processor.rs @@ -105,6 +105,7 @@ impl CommandProcessor { pub fn cmd_help(&self, _rest: Option, callback: UICallback) -> Result<(), String> { trace!("CommandProcessor::cmd_help"); self.ui_sender().add_node_event( + Level::Info, r#"Commands: exit/quit - exit the client disconnect - disconnect the client from the Veilid node @@ -186,8 +187,14 @@ reply - reply to an AppCall not handled directly by the server let ui = self.ui_sender(); spawn_detached_local(async move { match capi.server_debug(rest.unwrap_or_default()).await { - Ok(output) => ui.display_string_dialog("Debug Output", output, callback), - Err(e) => ui.display_string_dialog("Debug Error", e.to_string(), callback), + Ok(output) => { + ui.add_node_event(Level::Debug, output); + ui.send_callback(callback); + } + Err(e) => { + ui.add_node_event(Level::Error, e.to_string()); + ui.send_callback(callback); + } } }); Ok(()) @@ -206,7 +213,7 @@ reply - reply to an AppCall not handled directly by the server let log_level = match convert_loglevel(&rest.unwrap_or_default()) { Ok(v) => v, Err(e) => { - ui.add_node_event(format!("Failed to change log level: {}", e)); + ui.add_node_event(Level::Error, format!("Failed to change log level: {}", e)); ui.send_callback(callback); return; } @@ -239,7 +246,7 @@ reply - reply to an AppCall not handled directly by the server let (id, msg) = if let Some(second) = second { let id = match u64::from_str(&first) { Err(e) => { - ui.add_node_event(format!("invalid appcall id: {}", e)); + ui.add_node_event(Level::Error, format!("invalid appcall id: {}", e)); ui.send_callback(callback); return; } @@ -249,7 +256,7 @@ reply - reply to an AppCall not handled directly by the server } else { let id = match some_last_id { None => { - ui.add_node_event("must specify last call id".to_owned()); + ui.add_node_event(Level::Error, "must specify last call id".to_owned()); ui.send_callback(callback); return; } @@ -260,7 +267,7 @@ reply - reply to an AppCall not handled directly by the server let msg = if msg[0..1] == "#".to_owned() { match hex::decode(msg[1..].as_bytes().to_vec()) { Err(e) => { - ui.add_node_event(format!("invalid hex message: {}", e)); + ui.add_node_event(Level::Error, format!("invalid hex message: {}", e)); ui.send_callback(callback); return; } @@ -272,7 +279,10 @@ reply - reply to an AppCall not handled directly by the server let msglen = msg.len(); match capi.server_appcall_reply(id, msg).await { Ok(()) => { - ui.add_node_event(format!("reply sent to {} : {} bytes", id, msglen)); + ui.add_node_event( + Level::Info, + format!("reply sent to {} : {} bytes", id, msglen), + ); ui.send_callback(callback); return; } @@ -383,8 +393,8 @@ reply - reply to an AppCall not handled directly by the server // calls into ui //////////////////////////////////////////// - pub fn log_message(&self, message: String) { - self.inner().ui_sender.add_node_event(message); + pub fn log_message(&self, log_level: Level, message: String) { + self.inner().ui_sender.add_node_event(log_level, message); } pub fn update_attachment(&self, attachment: &json::JsonValue) { @@ -428,25 +438,30 @@ reply - reply to an AppCall not handled directly by the server )); } if !out.is_empty() { - self.inner().ui_sender.add_node_event(out); + self.inner().ui_sender.add_node_event(Level::Info, out); } } pub fn update_value_change(&self, value_change: &json::JsonValue) { let out = format!("Value change: {:?}", value_change.as_str().unwrap_or("???")); - self.inner().ui_sender.add_node_event(out); + self.inner().ui_sender.add_node_event(Level::Info, out); } pub fn update_log(&self, log: &json::JsonValue) { - self.inner().ui_sender.add_node_event(format!( - "{}: {}{}", - log["log_level"].as_str().unwrap_or("???"), - log["message"].as_str().unwrap_or("???"), - if let Some(bt) = log["backtrace"].as_str() { - format!("\nBacktrace:\n{}", bt) - } else { - "".to_owned() - } - )); + let log_level = + Level::from_str(log["log_level"].as_str().unwrap_or("error")).unwrap_or(Level::Error); + self.inner().ui_sender.add_node_event( + log_level, + format!( + "{}: {}{}", + log["log_level"].as_str().unwrap_or("???"), + log["message"].as_str().unwrap_or("???"), + if let Some(bt) = log["backtrace"].as_str() { + format!("\nBacktrace:\n{}", bt) + } else { + "".to_owned() + } + ), + ); } pub fn update_app_message(&self, msg: &json::JsonValue) { @@ -466,9 +481,10 @@ reply - reply to an AppCall not handled directly by the server hex::encode(message) }; - self.inner() - .ui_sender - .add_node_event(format!("AppMessage ({:?}): {}", msg["sender"], strmsg)); + self.inner().ui_sender.add_node_event( + Level::Info, + format!("AppMessage ({:?}): {}", msg["sender"], strmsg), + ); } pub fn update_app_call(&self, call: &json::JsonValue) { @@ -490,10 +506,13 @@ reply - reply to an AppCall not handled directly by the server let id = json_str_u64(&call["call_id"]); - self.inner().ui_sender.add_node_event(format!( - "AppCall ({:?}) id = {:016x} : {}", - call["sender"], id, strmsg - )); + self.inner().ui_sender.add_node_event( + Level::Info, + format!( + "AppCall ({:?}) id = {:016x} : {}", + call["sender"], id, strmsg + ), + ); self.inner_mut().last_call_id = Some(id); } diff --git a/veilid-cli/src/peers_table_view.rs b/veilid-cli/src/peers_table_view.rs index 2d158de6..6ba9639a 100644 --- a/veilid-cli/src/peers_table_view.rs +++ b/veilid-cli/src/peers_table_view.rs @@ -74,8 +74,14 @@ impl TableViewItem for json::JsonValue { Self: Sized, { match column { - PeerTableColumn::NodeId => self.to_column(column).cmp(&other.to_column(column)), - PeerTableColumn::Address => self.to_column(column).cmp(&other.to_column(column)), + PeerTableColumn::NodeId => self + .to_column(column) + .to_ascii_lowercase() + .cmp(&other.to_column(column).to_ascii_lowercase()), + PeerTableColumn::Address => self + .to_column(column) + .to_ascii_lowercase() + .cmp(&other.to_column(column).to_ascii_lowercase()), PeerTableColumn::LatencyAvg => json_str_u64(&self["peer_stats"]["latency"]["average"]) .cmp(&json_str_u64(&other["peer_stats"]["latency"]["average"])), PeerTableColumn::TransferDownAvg => { diff --git a/veilid-cli/src/settings.rs b/veilid-cli/src/settings.rs index a5382ea0..5cc9268f 100644 --- a/veilid-cli/src/settings.rs +++ b/veilid-cli/src/settings.rs @@ -27,23 +27,23 @@ interface: shadow: false borders: "simple" colors: - background : "#333D3D" - shadow : "#000000" - view : "#1c2323" - primary : "#a6d8d3" - secondary : "#8cb4b7" - tertiary : "#eeeeee" - title_primary : "#f93fbd" - title_secondary : "#ff0000" - highlight : "#f93fbd" - highlight_inactive : "#a6d8d3" - highlight_text : "#333333" + background : "black" + shadow : "black" + view : "black" + primary : "light cyan" + secondary : "cyan" + tertiary : "green" + title_primary : "light magenta" + title_secondary : "magenta" + highlight : "light white" + highlight_inactive : "white" + highlight_text : "black" log_colors: - trace : "#707070" - debug : "#a0a0a0" - info : "#5cd3c6" - warn : "#fedc50" - error : "#ff4a15" + trace : "light blue" + debug : "light green" + info : "white" + warn : "light yellow" + error : "light red" "### .replace( "%LOGGING_FILE_DIRECTORY%", diff --git a/veilid-cli/src/ui.rs b/veilid-cli/src/ui.rs index a38ff6dc..8ea3ab9e 100644 --- a/veilid-cli/src/ui.rs +++ b/veilid-cli/src/ui.rs @@ -12,10 +12,10 @@ use cursive::views::*; use cursive::Cursive; use cursive::CursiveRunnable; use cursive_flexi_logger_view::{CursiveLogWriter, FlexiLoggerView}; -//use cursive_multiplex::*; +// use cursive_multiplex::*; use std::collections::{HashMap, VecDeque}; +use std::io::Write; use thiserror::Error; - ////////////////////////////////////////////////////////////// /// struct Dirty { @@ -311,8 +311,7 @@ impl UI { .button("Close", move |s| { s.pop_layer(); close_cb(s); - }), //.wrap_with(CircularFocus::new) - //.wrap_tab(), + }), ); s.set_global_callback(cursive::event::Event::Key(Key::Esc), move |s| { s.set_global_callback(cursive::event::Event::Key(Key::Esc), UI::quit_handler); @@ -346,18 +345,19 @@ impl UI { return; } // run command - cursive_flexi_logger_view::push_to_log(StyledString::styled( + + cursive_flexi_logger_view::parse_lines_to_log( + ColorStyle::primary().into(), format!("> {}", text), - ColorStyle::primary(), - )); + ); match Self::run_command(s, text) { Ok(_) => {} Err(e) => { let color = *Self::inner_mut(s).log_colors.get(&Level::Error).unwrap(); - cursive_flexi_logger_view::push_to_log(StyledString::styled( + cursive_flexi_logger_view::parse_lines_to_log( + color.into(), format!(" Error: {}", e), - color, - )); + ); } } // save to history unless it's a duplicate @@ -455,6 +455,55 @@ impl UI { Self::command_processor(s).start_connection(); } + fn copy_to_clipboard>(s: &mut Cursive, text: S) { + if let Ok(mut clipboard) = arboard::Clipboard::new() { + // X11/Wayland/other system copy + if clipboard.set_text(text.as_ref()).is_ok() { + let color = *Self::inner_mut(s).log_colors.get(&Level::Info).unwrap(); + cursive_flexi_logger_view::parse_lines_to_log( + color.into(), + format!(">> Copied: {}", text.as_ref()), + ); + } else { + let color = *Self::inner_mut(s).log_colors.get(&Level::Warn).unwrap(); + cursive_flexi_logger_view::parse_lines_to_log( + color.into(), + format!(">> Could not copy to clipboard"), + ); + } + } else { + // OSC52 clipboard copy for terminals + if std::io::stdout() + .write_all( + format!( + "\x1B]52;c;{}\x07", + data_encoding::BASE64.encode(text.as_ref().as_bytes()), + ) + .as_bytes(), + ) + .is_ok() + { + if std::io::stdout().flush().is_ok() { + let color = *Self::inner_mut(s).log_colors.get(&Level::Info).unwrap(); + cursive_flexi_logger_view::parse_lines_to_log( + color.into(), + format!(">> Copied: {}", text.as_ref()), + ); + } + } + } + } + + fn on_submit_peers_table_view(s: &mut Cursive, _row: usize, index: usize) { + let peers_table_view = UI::peers(s); + let node_id = peers_table_view + .borrow_item(index) + .map(|j| j["node_ids"][0].to_string()); + if let Some(node_id) = node_id { + Self::copy_to_clipboard(s, node_id); + } + } + fn show_connection_dialog(s: &mut Cursive, state: ConnectionState) -> bool { let mut inner = Self::inner_mut(s); @@ -644,7 +693,28 @@ impl UI { fn refresh_peers(s: &mut Cursive) { let mut peers = UI::peers(s); let inner = Self::inner_mut(s); + let sel_item = peers.item(); + let sel_item_text = peers + .item() + .map(|x| peers.borrow_items()[x]["node_ids"][0].clone()); + peers.set_items_stable(inner.ui_state.peers_state.get().clone()); + + let mut selected = false; + if let Some(sel_item_text) = sel_item_text { + // First select by name + for n in 0..peers.borrow_items().len() { + if peers.borrow_items()[n]["node_ids"][0] == sel_item_text { + peers.set_selected_item(n); + selected = true; + } + } + } + if !selected { + if let Some(sel_item) = sel_item { + peers.set_selected_item(sel_item); + } + } } fn update_cb(s: &mut Cursive) { @@ -707,9 +777,6 @@ impl UI { // Instantiate the cursive runnable let runnable = CursiveRunnable::new( || -> Result, Box> { - #[cfg(feature = "macos")] - let backend = cursive::backends::curses::n::Backend::init().unwrap(); - #[cfg(not(feature = "macos"))] let backend = cursive::backends::crossterm::Backend::init().unwrap(); let buffered_backend = cursive_buffered_backend::BufferedBackend::new(backend); Ok(Box::new(buffered_backend)) @@ -737,6 +804,11 @@ impl UI { })), }; + let ui_sender = UISender { + inner: this.inner.clone(), + cb_sink, + }; + let mut inner = this.inner.lock(); // Make the inner object accessible in callbacks easily @@ -750,12 +822,14 @@ impl UI { .with_name("node-events-panel") .full_screen(); - let peers_table_view = PeersTableView::new() + let mut peers_table_view = PeersTableView::new() .column(PeerTableColumn::NodeId, "Node Id", |c| c.width(48)) .column(PeerTableColumn::Address, "Address", |c| c) .column(PeerTableColumn::LatencyAvg, "Ping", |c| c.width(8)) .column(PeerTableColumn::TransferDownAvg, "Down", |c| c.width(8)) - .column(PeerTableColumn::TransferUpAvg, "Up", |c| c.width(8)) + .column(PeerTableColumn::TransferUpAvg, "Up", |c| c.width(8)); + peers_table_view.set_on_submit(UI::on_submit_peers_table_view); + let peers_table_view = peers_table_view .with_name("peers") .full_width() .min_height(8); @@ -832,8 +906,7 @@ impl UI { drop(inner); - let inner = this.inner.clone(); - (this, UISender { inner, cb_sink }) + (this, ui_sender) } pub fn cursive_flexi_logger(&self) -> Box { let mut flv = cursive_flexi_logger_view::cursive_flexi_logger(self.siv.cb_sink().clone()); @@ -906,7 +979,7 @@ impl UISender { started: bool, bps_down: u64, bps_up: u64, - peers: Vec, + mut peers: Vec, ) { { let mut inner = self.inner.lock(); @@ -915,6 +988,11 @@ impl UISender { ((bps_down as f64) / 1000.0f64) as f32, ((bps_up as f64) / 1000.0f64) as f32, )); + peers.sort_by(|a, b| { + a["node_ids"][0] + .to_string() + .cmp(&b["node_ids"][0].to_string()) + }); inner.ui_state.peers_state.set(peers); } let _ = self.cb_sink.send(Box::new(UI::update_cb)); @@ -922,10 +1000,18 @@ impl UISender { pub fn set_config(&mut self, config: &json::JsonValue) { let mut inner = self.inner.lock(); - inner - .ui_state - .node_id - .set(config["network"]["routing_table"]["node_id"].to_string()); + let node_ids = &config["network"]["routing_table"]["node_id"]; + + let mut node_id_str = String::new(); + for l in 0..node_ids.len() { + let nid = &node_ids[l]; + if !node_id_str.is_empty() { + node_id_str.push_str(" "); + } + node_id_str.push_str(nid.to_string().as_ref()); + } + + inner.ui_state.node_id.set(node_id_str); } pub fn set_connection_state(&mut self, state: ConnectionState) { { @@ -935,17 +1021,11 @@ impl UISender { let _ = self.cb_sink.send(Box::new(UI::update_cb)); } - pub fn add_node_event(&self, event: String) { + pub fn add_node_event(&self, log_color: Level, event: String) { { let inner = self.inner.lock(); - let color = *inner.log_colors.get(&Level::Info).unwrap(); - let mut starting_style: Style = color.into(); - for line in event.lines() { - let (spanned_string, end_style) = - cursive::utils::markup::ansi::parse_with_starting_style(starting_style, line); - cursive_flexi_logger_view::push_to_log(spanned_string); - starting_style = end_style; - } + let color = *inner.log_colors.get(&log_color).unwrap(); + cursive_flexi_logger_view::parse_lines_to_log(color.into(), event); } let _ = self.cb_sink.send(Box::new(UI::update_cb)); } diff --git a/veilid-core/src/crypto/byte_array_types.rs b/veilid-core/src/crypto/byte_array_types.rs index 8a1f7a01..5590e467 100644 --- a/veilid-core/src/crypto/byte_array_types.rs +++ b/veilid-core/src/crypto/byte_array_types.rs @@ -130,6 +130,7 @@ macro_rules! byte_array_type { Self { bytes } } + // Big endian bit ordering pub fn bit(&self, index: usize) -> bool { assert!(index < ($size * 8)); let bi = index / 8; @@ -152,6 +153,7 @@ macro_rules! byte_array_type { None } + // Big endian nibble ordering pub fn nibble(&self, index: usize) -> u8 { assert!(index < ($size * 2)); let bi = index / 2; diff --git a/veilid-core/src/crypto/tests/test_types.rs b/veilid-core/src/crypto/tests/test_types.rs index 72813ea0..3a544186 100644 --- a/veilid-core/src/crypto/tests/test_types.rs +++ b/veilid-core/src/crypto/tests/test_types.rs @@ -353,10 +353,40 @@ async fn test_operations(vcrypto: CryptoSystemVersion) { assert_eq!(d4.first_nonzero_bit(), Some(0)); } +pub async fn test_crypto_key_ordering() { + let k1 = CryptoKey::new([ + 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ]); + let k2 = CryptoKey::new([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ]); + let k3 = CryptoKey::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 128, + ]); + let k4 = CryptoKey::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, + ]); + let k5 = CryptoKey::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ]); + + assert!(k2 < k1); + assert!(k3 < k2); + assert!(k4 < k3); + assert!(k5 < k4); +} + pub async fn test_all() { let api = crypto_tests_startup().await; let crypto = api.crypto().unwrap(); + test_crypto_key_ordering().await; + // Test versions for v in VALID_CRYPTO_KINDS { let vcrypto = crypto.get(v).unwrap(); diff --git a/veilid-core/src/routing_table/find_peers.rs b/veilid-core/src/routing_table/find_peers.rs index 54a45796..2373e9be 100644 --- a/veilid-core/src/routing_table/find_peers.rs +++ b/veilid-core/src/routing_table/find_peers.rs @@ -51,6 +51,7 @@ impl RoutingTable { return NetworkResult::invalid_message("unsupported cryptosystem"); }; let own_distance = vcrypto.distance(&own_node_id.value, &key.value); + let vcrypto2 = vcrypto.clone(); let filter = Box::new( move |rti: &RoutingTableInner, opt_entry: Option>| { @@ -98,6 +99,46 @@ impl RoutingTable { }, ); + // xxx test + // Validate peers returned are, in fact, closer to the key than the node we sent this to + let valid = match Self::verify_peers_closer(vcrypto2, own_node_id, key, &closest_nodes) { + Ok(v) => v, + Err(e) => { + panic!("missing cryptosystem in peers node ids: {}", e); + } + }; + if !valid { + panic!("non-closer peers returned"); + } + NetworkResult::value(closest_nodes) } + + /// Determine if set of peers is closer to key_near than key_far + pub(crate) fn verify_peers_closer( + vcrypto: CryptoSystemVersion, + key_far: TypedKey, + key_near: TypedKey, + peers: &[PeerInfo], + ) -> EyreResult { + let kind = vcrypto.kind(); + + if key_far.kind != kind || key_near.kind != kind { + bail!("keys all need the same cryptosystem"); + } + + let mut closer = true; + for peer in peers { + let Some(key_peer) = peer.node_ids().get(kind) else { + bail!("peers need to have a key with the same cryptosystem"); + }; + let d_near = vcrypto.distance(&key_near.value, &key_peer.value); + let d_far = vcrypto.distance(&key_far.value, &key_peer.value); + if d_far < d_near { + closer = false; + } + } + + Ok(closer) + } } diff --git a/veilid-core/src/rpc_processor/destination.rs b/veilid-core/src/rpc_processor/destination.rs index e30052c8..9ffe6995 100644 --- a/veilid-core/src/rpc_processor/destination.rs +++ b/veilid-core/src/rpc_processor/destination.rs @@ -29,6 +29,13 @@ pub enum Destination { } impl Destination { + pub fn target(&self) -> Option { + match self { + Destination::Direct { target, safety_selection: _ } => Some(target.clone()), + Destination::Relay { relay:_, target, safety_selection: _ } => Some(target.clone()), + Destination::PrivateRoute { private_route:_, safety_selection:_ } => None, + } + } pub fn direct(target: NodeRef) -> Self { let sequencing = target.sequencing(); Self::Direct { diff --git a/veilid-core/src/rpc_processor/fanout_call.rs b/veilid-core/src/rpc_processor/fanout_call.rs index 3486e8e2..49b8b64e 100644 --- a/veilid-core/src/rpc_processor/fanout_call.rs +++ b/veilid-core/src/rpc_processor/fanout_call.rs @@ -88,6 +88,7 @@ where for cn in &ctx.closest_nodes { if cn.same_entry(&nn) { dup = true; + break; } } if !dup { @@ -125,6 +126,7 @@ where // New fanout call candidate found next_node = Some(cn.clone()); ctx.called_nodes.add(key); + break; } } } diff --git a/veilid-core/src/rpc_processor/mod.rs b/veilid-core/src/rpc_processor/mod.rs index 805b62e4..f9b41323 100644 --- a/veilid-core/src/rpc_processor/mod.rs +++ b/veilid-core/src/rpc_processor/mod.rs @@ -395,7 +395,7 @@ impl RPCProcessor { ////////////////////////////////////////////////////////////////////// /// Determine if a SignedNodeInfo can be placed into the specified routing domain - fn filter_node_info( + fn verify_node_info( &self, routing_domain: RoutingDomain, signed_node_info: &SignedNodeInfo, @@ -1348,7 +1348,7 @@ impl RPCProcessor { // Ensure the sender peer info is for the actual sender specified in the envelope // Sender PeerInfo was specified, update our routing table with it - if !self.filter_node_info(routing_domain, sender_peer_info.signed_node_info()) { + if !self.verify_node_info(routing_domain, sender_peer_info.signed_node_info()) { return Ok(NetworkResult::invalid_message( "sender peerinfo has invalid peer scope", )); diff --git a/veilid-core/src/rpc_processor/rpc_app_call.rs b/veilid-core/src/rpc_processor/rpc_app_call.rs index 55399a0a..f3de4828 100644 --- a/veilid-core/src/rpc_processor/rpc_app_call.rs +++ b/veilid-core/src/rpc_processor/rpc_app_call.rs @@ -29,9 +29,9 @@ impl RPCProcessor { let app_call_a = match kind { RPCOperationKind::Answer(a) => match a.destructure() { RPCAnswerDetail::AppCallA(a) => a, - _ => return Err(RPCError::invalid_format("not an appcall answer")), + _ => return Ok(NetworkResult::invalid_message("not an appcall answer")), }, - _ => return Err(RPCError::invalid_format("not an answer")), + _ => return Ok(NetworkResult::invalid_message("not an answer")), }; let a_message = app_call_a.destructure(); diff --git a/veilid-core/src/rpc_processor/rpc_find_node.rs b/veilid-core/src/rpc_processor/rpc_find_node.rs index fe2b416f..8b7ded6b 100644 --- a/veilid-core/src/rpc_processor/rpc_find_node.rs +++ b/veilid-core/src/rpc_processor/rpc_find_node.rs @@ -46,17 +46,17 @@ impl RPCProcessor { let find_node_a = match kind { RPCOperationKind::Answer(a) => match a.destructure() { RPCAnswerDetail::FindNodeA(a) => a, - _ => return Err(RPCError::invalid_format("not a find_node answer")), + _ => return Ok(NetworkResult::invalid_message("not a find_node answer")), }, - _ => return Err(RPCError::invalid_format("not an answer")), + _ => return Ok(NetworkResult::invalid_message("not an answer")), }; // Verify peers are in the correct peer scope let peers = find_node_a.destructure(); for peer_info in &peers { - if !self.filter_node_info(RoutingDomain::PublicInternet, peer_info.signed_node_info()) { - return Err(RPCError::invalid_format( + if !self.verify_node_info(RoutingDomain::PublicInternet, peer_info.signed_node_info()) { + return Ok(NetworkResult::invalid_message( "find_node response has invalid peer scope", )); } diff --git a/veilid-core/src/rpc_processor/rpc_get_value.rs b/veilid-core/src/rpc_processor/rpc_get_value.rs index 7e68b172..cd82f067 100644 --- a/veilid-core/src/rpc_processor/rpc_get_value.rs +++ b/veilid-core/src/rpc_processor/rpc_get_value.rs @@ -24,32 +24,32 @@ impl RPCProcessor { last_descriptor: Option, ) -> Result>, RPCError> { // Ensure destination never has a private route - if matches!( - dest, - Destination::PrivateRoute { - private_route: _, - safety_selection: _ - } - ) { + // and get the target noderef so we can validate the response + let Some(target) = dest.target() else { return Err(RPCError::internal( - "Never send get value requests over private routes", + "Never send set value requests over private routes", )); - } + }; + // Get the target node id + let Some(vcrypto) = self.crypto.get(key.kind) else { + return Err(RPCError::internal("unsupported cryptosystem")); + }; + let Some(target_node_id) = target.node_ids().get(key.kind) else { + return Err(RPCError::internal("No node id for crypto kind")); + }; + + // Send the getvalue question let get_value_q = RPCOperationGetValueQ::new(key, subkey, last_descriptor.is_none()); let question = RPCQuestion::new( network_result_try!(self.get_destination_respond_to(&dest)?), RPCQuestionDetail::GetValueQ(get_value_q), ); - let Some(vcrypto) = self.crypto.get(key.kind) else { - return Err(RPCError::internal("unsupported cryptosystem")); - }; - // Send the getvalue question let question_context = QuestionContext::GetValue(ValidateGetValueContext { last_descriptor, subkey, - vcrypto, + vcrypto: vcrypto.clone(), }); let waitable_reply = network_result_try!( @@ -68,13 +68,27 @@ impl RPCProcessor { let get_value_a = match kind { RPCOperationKind::Answer(a) => match a.destructure() { RPCAnswerDetail::GetValueA(a) => a, - _ => return Err(RPCError::invalid_format("not a getvalue answer")), + _ => return Ok(NetworkResult::invalid_message("not a getvalue answer")), }, - _ => return Err(RPCError::invalid_format("not an answer")), + _ => return Ok(NetworkResult::invalid_message("not an answer")), }; let (value, peers, descriptor) = get_value_a.destructure(); + // Validate peers returned are, in fact, closer to the key than the node we sent this to + let valid = match RoutingTable::verify_peers_closer(vcrypto, target_node_id, key, &peers) { + Ok(v) => v, + Err(e) => { + return Ok(NetworkResult::invalid_message(format!( + "missing cryptosystem in peers node ids: {}", + e + ))); + } + }; + if !valid { + return Ok(NetworkResult::invalid_message("non-closer peers returned")); + } + Ok(NetworkResult::value(Answer::new( latency, GetValueAnswer { diff --git a/veilid-core/src/rpc_processor/rpc_route.rs b/veilid-core/src/rpc_processor/rpc_route.rs index 64bc884e..b397f26c 100644 --- a/veilid-core/src/rpc_processor/rpc_route.rs +++ b/veilid-core/src/rpc_processor/rpc_route.rs @@ -62,7 +62,7 @@ impl RPCProcessor { ) -> Result, RPCError> { // Make sure hop count makes sense if next_private_route.hop_count as usize > self.unlocked_inner.max_route_hop_count { - return Err(RPCError::protocol( + return Ok(NetworkResult::invalid_message( "Private route hop count too high to process", )); } @@ -110,9 +110,9 @@ impl RPCProcessor { // Now that things are valid, decrypt the routed operation with DEC(nonce, DH(the SR's public key, the PR's (or node's) secret) // xxx: punish nodes that send messages that fail to decrypt eventually? How to do this for safety routes? let node_id_secret = self.routing_table.node_id_secret_key(remote_sr_pubkey.kind); - let dh_secret = vcrypto - .cached_dh(&remote_sr_pubkey.value, &node_id_secret) - .map_err(RPCError::protocol)?; + let Ok(dh_secret) = vcrypto.cached_dh(&remote_sr_pubkey.value, &node_id_secret) else { + return Ok(NetworkResult::invalid_message("dh failed for remote safety route for safety routed operation")); + }; let body = match vcrypto.decrypt_aead( routed_operation.data(), routed_operation.nonce(), @@ -183,19 +183,18 @@ impl RPCProcessor { // Now that things are valid, decrypt the routed operation with DEC(nonce, DH(the SR's public key, the PR's (or node's) secret) // xxx: punish nodes that send messages that fail to decrypt eventually. How to do this for private routes? - let dh_secret = vcrypto - .cached_dh(&remote_sr_pubkey.value, &secret_key) - .map_err(RPCError::protocol)?; - let body = vcrypto + let Ok(dh_secret) = vcrypto.cached_dh(&remote_sr_pubkey.value, &secret_key) else { + return Ok(NetworkResult::invalid_message("dh failed for remote safety route for private routed operation")); + }; + let Ok(body) = vcrypto .decrypt_aead( routed_operation.data(), routed_operation.nonce(), &dh_secret, None, - ) - .map_err(RPCError::map_internal( - "decryption of routed operation failed", - ))?; + ) else { + return Ok(NetworkResult::invalid_message("decryption of routed operation failed")); + }; // Pass message to RPC system self.enqueue_private_routed_message( @@ -401,37 +400,48 @@ impl RPCProcessor { SafetyRouteHops::Data(ref route_hop_data) => { // Decrypt the blob with DEC(nonce, DH(the SR's public key, this hop's secret) let node_id_secret = self.routing_table.node_id_secret_key(crypto_kind); - let dh_secret = vcrypto - .cached_dh(&safety_route.public_key.value, &node_id_secret) - .map_err(RPCError::protocol)?; - let mut dec_blob_data = vcrypto + let Ok(dh_secret) = vcrypto + .cached_dh(&safety_route.public_key.value, &node_id_secret) else { + return Ok(NetworkResult::invalid_message("dh failed for safety route hop")); + }; + let Ok(mut dec_blob_data) = vcrypto .decrypt_aead( &route_hop_data.blob, &route_hop_data.nonce, &dh_secret, None, ) - .map_err(RPCError::protocol)?; + else { + return Ok(NetworkResult::invalid_message("failed to decrypt route hop data for safety route hop")); + }; // See if this is last hop in safety route, if so, we're decoding a PrivateRoute not a RouteHop let Some(dec_blob_tag) = dec_blob_data.pop() else { return Ok(NetworkResult::invalid_message("no bytes in blob")); }; - let dec_blob_reader = RPCMessageData::new(dec_blob_data).get_reader()?; + let Ok(dec_blob_reader) = RPCMessageData::new(dec_blob_data).get_reader() else { + return Ok(NetworkResult::invalid_message("Failed to decode RPCMessageData from blob")); + }; // Decode the blob appropriately if dec_blob_tag == 1 { // PrivateRoute let private_route = { - let pr_reader = dec_blob_reader - .get_root::() - .map_err(RPCError::protocol)?; - decode_private_route(&pr_reader)? + let Ok(pr_reader) = dec_blob_reader + .get_root::() else { + return Ok(NetworkResult::invalid_message("failed to get private route reader for blob")); + }; + let Ok(private_route) = decode_private_route(&pr_reader) else { + return Ok(NetworkResult::invalid_message("failed to decode private route")); + }; + private_route }; // Validate the private route - private_route.validate(self.crypto.clone()).map_err(RPCError::protocol)?; + if let Err(_) = private_route.validate(self.crypto.clone()) { + return Ok(NetworkResult::invalid_message("failed to validate private route")); + } // Switching from full safety route to private route first hop network_result_try!( @@ -445,14 +455,20 @@ impl RPCProcessor { } else if dec_blob_tag == 0 { // RouteHop let route_hop = { - let rh_reader = dec_blob_reader - .get_root::() - .map_err(RPCError::protocol)?; - decode_route_hop(&rh_reader)? + let Ok(rh_reader) = dec_blob_reader + .get_root::() else { + return Ok(NetworkResult::invalid_message("failed to get route hop reader for blob")); + }; + let Ok(route_hop) = decode_route_hop(&rh_reader) else { + return Ok(NetworkResult::invalid_message("failed to decode route hop")); + }; + route_hop }; // Validate the route hop - route_hop.validate(self.crypto.clone()).map_err(RPCError::protocol)?; + if let Err(_) = route_hop.validate(self.crypto.clone()) { + return Ok(NetworkResult::invalid_message("failed to validate route hop")); + } // Continue the full safety route with another hop network_result_try!( diff --git a/veilid-core/src/rpc_processor/rpc_set_value.rs b/veilid-core/src/rpc_processor/rpc_set_value.rs index cf4a61be..e36852af 100644 --- a/veilid-core/src/rpc_processor/rpc_set_value.rs +++ b/veilid-core/src/rpc_processor/rpc_set_value.rs @@ -14,7 +14,6 @@ impl RPCProcessor { /// Because this leaks information about the identity of the node itself, /// replying to this request received over a private route will leak /// the identity of the node and defeat the private route. - #[instrument(level = "trace", skip(self), ret, err)] pub async fn rpc_call_set_value( self, dest: Destination, @@ -25,18 +24,22 @@ impl RPCProcessor { send_descriptor: bool, ) -> Result>, RPCError> { // Ensure destination never has a private route - if matches!( - dest, - Destination::PrivateRoute { - private_route: _, - safety_selection: _ - } - ) { + // and get the target noderef so we can validate the response + let Some(target) = dest.target() else { return Err(RPCError::internal( "Never send set value requests over private routes", )); - } + }; + // Get the target node id + let Some(vcrypto) = self.crypto.get(key.kind) else { + return Err(RPCError::internal("unsupported cryptosystem")); + }; + let Some(target_node_id) = target.node_ids().get(key.kind) else { + return Err(RPCError::internal("No node id for crypto kind")); + }; + + // Send the setvalue question let set_value_q = RPCOperationSetValueQ::new( key, subkey, @@ -51,17 +54,11 @@ impl RPCProcessor { network_result_try!(self.get_destination_respond_to(&dest)?), RPCQuestionDetail::SetValueQ(set_value_q), ); - let Some(vcrypto) = self.crypto.get(key.kind) else { - return Err(RPCError::internal("unsupported cryptosystem")); - }; - - // Send the setvalue question let question_context = QuestionContext::SetValue(ValidateSetValueContext { descriptor, subkey, - vcrypto, + vcrypto: vcrypto.clone(), }); - let waitable_reply = network_result_try!( self.question(dest, question, Some(question_context)) .await? @@ -78,13 +75,27 @@ impl RPCProcessor { let set_value_a = match kind { RPCOperationKind::Answer(a) => match a.destructure() { RPCAnswerDetail::SetValueA(a) => a, - _ => return Err(RPCError::invalid_format("not a setvalue answer")), + _ => return Ok(NetworkResult::invalid_message("not a setvalue answer")), }, - _ => return Err(RPCError::invalid_format("not an answer")), + _ => return Ok(NetworkResult::invalid_message("not an answer")), }; let (set, value, peers) = set_value_a.destructure(); + // Validate peers returned are, in fact, closer to the key than the node we sent this to + let valid = match RoutingTable::verify_peers_closer(vcrypto, target_node_id, key, &peers) { + Ok(v) => v, + Err(e) => { + return Ok(NetworkResult::invalid_message(format!( + "missing cryptosystem in peers node ids: {}", + e + ))); + } + }; + if !valid { + return Ok(NetworkResult::invalid_message("non-closer peers returned")); + } + Ok(NetworkResult::value(Answer::new( latency, SetValueAnswer { set, value, peers }, diff --git a/veilid-core/src/rpc_processor/rpc_status.rs b/veilid-core/src/rpc_processor/rpc_status.rs index 179508ce..323789b3 100644 --- a/veilid-core/src/rpc_processor/rpc_status.rs +++ b/veilid-core/src/rpc_processor/rpc_status.rs @@ -119,9 +119,9 @@ impl RPCProcessor { let status_a = match kind { RPCOperationKind::Answer(a) => match a.destructure() { RPCAnswerDetail::StatusA(a) => a, - _ => return Err(RPCError::invalid_format("not a status answer")), + _ => return Ok(NetworkResult::invalid_message("not a status answer")), }, - _ => return Err(RPCError::invalid_format("not an answer")), + _ => return Ok(NetworkResult::invalid_message("not an answer")), }; let (a_node_status, sender_info) = status_a.destructure(); diff --git a/veilid-core/src/storage_manager/get_value.rs b/veilid-core/src/storage_manager/get_value.rs index 3d5584e7..b05e4de5 100644 --- a/veilid-core/src/storage_manager/get_value.rs +++ b/veilid-core/src/storage_manager/get_value.rs @@ -1,7 +1,7 @@ use super::*; -/// The context of the do_get_value operation -struct DoGetValueContext { +/// The context of the outbound_get_value operation +struct OutboundGetValueContext { /// The latest value of the subkey, may be the value passed in pub value: Option, /// The consensus count for the value we have received @@ -42,7 +42,7 @@ impl StorageManager { } else { None }; - let context = Arc::new(Mutex::new(DoGetValueContext { + let context = Arc::new(Mutex::new(OutboundGetValueContext { value: last_subkey_result.value, value_count: 0, descriptor: last_subkey_result.descriptor.clone(), diff --git a/veilid-core/src/storage_manager/mod.rs b/veilid-core/src/storage_manager/mod.rs index dbef0bc8..56a906bc 100644 --- a/veilid-core/src/storage_manager/mod.rs +++ b/veilid-core/src/storage_manager/mod.rs @@ -341,7 +341,6 @@ impl StorageManager { } else { ValueData::new(data, writer.key) }; - let seq = value_data.seq(); // Validate with schema if !schema.check_subkey_value_data(descriptor.owner(), subkey, &value_data) { @@ -374,7 +373,6 @@ impl StorageManager { drop(inner); // Use the safety selection we opened the record with - let final_signed_value_data = self .outbound_set_value( rpc_processor, @@ -386,13 +384,12 @@ impl StorageManager { ) .await?; - // If we got a new value back then write it to the opened record - if final_signed_value_data.value_data().seq() != seq { - let mut inner = self.lock().await?; - inner - .handle_set_local_value(key, subkey, final_signed_value_data.clone()) - .await?; - } + // Whatever record we got back, store it locally, might be newer than the one we asked to save + let mut inner = self.lock().await?; + inner + .handle_set_local_value(key, subkey, final_signed_value_data.clone()) + .await?; + Ok(Some(final_signed_value_data.into_value_data())) } diff --git a/veilid-core/src/storage_manager/set_value.rs b/veilid-core/src/storage_manager/set_value.rs index 896a623d..0d79dfe1 100644 --- a/veilid-core/src/storage_manager/set_value.rs +++ b/veilid-core/src/storage_manager/set_value.rs @@ -1,7 +1,7 @@ use super::*; -/// The context of the do_get_value operation -struct DoSetValueContext { +/// The context of the outbound_set_value operation +struct OutboundSetValueContext { /// The latest value of the subkey, may be the value passed in pub value: SignedValueData, /// The consensus count for the value we have received @@ -37,7 +37,7 @@ impl StorageManager { // Make do-set-value answer context let schema = descriptor.schema()?; - let context = Arc::new(Mutex::new(DoSetValueContext { + let context = Arc::new(Mutex::new(OutboundSetValueContext { value, value_count: 0, schema, diff --git a/veilid-python/tests/test_dht.py b/veilid-python/tests/test_dht.py index d7f196fb..af17bc64 100644 --- a/veilid-python/tests/test_dht.py +++ b/veilid-python/tests/test_dht.py @@ -1,74 +1,77 @@ -# # Routing context veilid tests +# Routing context veilid tests -# import veilid -# import pytest -# import asyncio -# import json -# from . import * +import veilid +import pytest +import asyncio +import json +from . import * -# ################################################################## -# BOGUS_KEY = veilid.TypedKey.from_value(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.PublicKey.from_bytes(b' ')) +################################################################## +BOGUS_KEY = veilid.TypedKey.from_value(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.PublicKey.from_bytes(b' ')) -# @pytest.mark.asyncio -# async def test_get_dht_value_unopened(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# with pytest.raises(veilid.VeilidAPIError): -# out = await rc.get_dht_value(BOGUS_KEY, veilid.ValueSubkey(0), False) +@pytest.mark.asyncio +async def test_get_dht_value_unopened(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + with pytest.raises(veilid.VeilidAPIError): + out = await rc.get_dht_value(BOGUS_KEY, veilid.ValueSubkey(0), False) -# @pytest.mark.asyncio -# async def test_open_dht_record_nonexistent_no_writer(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# with pytest.raises(veilid.VeilidAPIError): -# out = await rc.open_dht_record(BOGUS_KEY, None) +@pytest.mark.asyncio +async def test_open_dht_record_nonexistent_no_writer(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + with pytest.raises(veilid.VeilidAPIError): + out = await rc.open_dht_record(BOGUS_KEY, None) -# @pytest.mark.asyncio -# async def test_close_dht_record_nonexistent(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# with pytest.raises(veilid.VeilidAPIError): -# await rc.close_dht_record(BOGUS_KEY) +@pytest.mark.asyncio +async def test_close_dht_record_nonexistent(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + with pytest.raises(veilid.VeilidAPIError): + await rc.close_dht_record(BOGUS_KEY) -# @pytest.mark.asyncio -# async def test_delete_dht_record_nonexistent(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# with pytest.raises(veilid.VeilidAPIError): -# await rc.delete_dht_record(BOGUS_KEY) +@pytest.mark.asyncio +async def test_delete_dht_record_nonexistent(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + with pytest.raises(veilid.VeilidAPIError): + await rc.delete_dht_record(BOGUS_KEY) -# @pytest.mark.asyncio -# async def test_create_delete_dht_record_simple(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# rec = await rc.create_dht_record(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.DHTSchema.dflt(1)) -# await rc.close_dht_record(rec.key) -# await rc.delete_dht_record(rec.key) +@pytest.mark.asyncio +async def test_create_delete_dht_record_simple(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + rec = await rc.create_dht_record(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.DHTSchema.dflt(1)) + await rc.close_dht_record(rec.key) + await rc.delete_dht_record(rec.key) -# @pytest.mark.asyncio -# async def test_get_dht_value_nonexistent(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# rec = await rc.create_dht_record(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.DHTSchema.dflt(1)) -# assert await rc.get_dht_value(rec.key, 0, False) == None -# await rc.close_dht_record(rec.key) -# await rc.delete_dht_record(rec.key) +@pytest.mark.asyncio +async def test_get_dht_value_nonexistent(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + rec = await rc.create_dht_record(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.DHTSchema.dflt(1)) + assert await rc.get_dht_value(rec.key, 0, False) == None + await rc.close_dht_record(rec.key) + await rc.delete_dht_record(rec.key) -# @pytest.mark.asyncio -# async def test_set_get_dht_value(api_connection: veilid.VeilidAPI): -# rc = await api_connection.new_routing_context() -# async with rc: -# rec = await rc.create_dht_record(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.DHTSchema.dflt(1)) +@pytest.mark.asyncio +async def test_set_get_dht_value(api_connection: veilid.VeilidAPI): + rc = await api_connection.new_routing_context() + async with rc: + rec = await rc.create_dht_record(veilid.CryptoKind.CRYPTO_KIND_VLD0, veilid.DHTSchema.dflt(1)) -# vd = await rc.set_dht_value(rec.key, 0, b"BLAH BLAH BLAH") -# assert vd != None + vd = await rc.set_dht_value(rec.key, 0, b"BLAH BLAH BLAH") + assert vd != None -# vd2 = await rc.get_dht_value(rec.key, 0, False) -# assert vd2 != None + vd2 = await rc.get_dht_value(rec.key, 0, False) + assert vd2 != None -# assert vd == vd2 + print("vd: {}", vd.__dict__) + print("vd2: {}", vd2.__dict__) -# await rc.close_dht_record(rec.key) -# await rc.delete_dht_record(rec.key) + assert vd == vd2 + + await rc.close_dht_record(rec.key) + await rc.delete_dht_record(rec.key) diff --git a/veilid-python/veilid/types.py b/veilid-python/veilid/types.py index d1766fab..7faa579a 100644 --- a/veilid-python/veilid/types.py +++ b/veilid-python/veilid/types.py @@ -2,6 +2,7 @@ import base64 import json from enum import StrEnum from typing import Any, Optional, Self, Tuple +from functools import total_ordering #################################################################### @@ -206,6 +207,7 @@ class ValueSeqNum(int): #################################################################### +@total_ordering class VeilidVersion: _major: int _minor: int @@ -216,6 +218,25 @@ class VeilidVersion: self._minor = minor self._patch = patch + def __lt__(self, other): + if other is None: + return False + if self._major < other._major: + return True + if self._major > other._major: + return False + if self._minor < other._minor: + return True + if self._minor > other._minor: + return False + if self._patch < other._patch: + return True + return False + + def __eq__(self, other): + return isinstance(other, VeilidVersion) and self.data == other.data and self.seq == other.seq and self.writer == other.writer + + @property def major(self): return self._major @@ -323,6 +344,7 @@ class DHTRecordDescriptor: return self.__dict__ +@total_ordering class ValueData: seq: ValueSeqNum data: bytes @@ -333,6 +355,24 @@ class ValueData: self.data = data self.writer = writer + def __lt__(self, other): + if other is None: + return true + if self.data < other.data: + return True + if self.data > other.data: + return False + if self.seq < other.seq: + return True + if self.seq > other.seq: + return False + if self.writer < other.writer: + return True + return False + + def __eq__(self, other): + return isinstance(other, ValueData) and self.data == other.data and self.seq == other.seq and self.writer == other.writer + @classmethod def from_json(cls, j: dict) -> Self: return cls(