From ec130d38b913bb254b64a6bead8b87889735f47f Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 30 Apr 2023 06:51:37 -0700 Subject: [PATCH 01/85] Support running server side with sudo This allows forwarding ports that you would otherwise not be able to see. More dangerous, probably not what you want most of the time, but OK for now. (I continue to resist adding clap as a dependency.) --- src/client/mod.rs | 21 ++++++++++----- src/main.rs | 69 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index da3a15d..567a186 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -275,6 +275,7 @@ async fn client_main( async fn spawn_ssh( server: &str, + sudo: bool, ) -> Result<(tokio::process::Child, u16), std::io::Error> { let socks_port = { let listener = TcpListener::bind("127.0.0.1:0").await?; @@ -285,9 +286,11 @@ async fn spawn_ssh( cmd.arg("-T") .arg("-D") .arg(socks_port.to_string()) - .arg(server) - .arg("fwd") - .arg("--server"); + .arg(server); + if sudo { + cmd.arg("sudo"); + } + cmd.arg("fwd").arg("--server"); cmd.stdout(std::process::Stdio::piped()); cmd.stdin(std::process::Stdio::piped()); @@ -314,12 +317,16 @@ fn is_sigint(status: std::process::ExitStatus) -> bool { } } -async fn client_connect_loop(remote: &str, events: mpsc::Sender) { +async fn client_connect_loop( + remote: &str, + sudo: bool, + events: mpsc::Sender, +) { loop { _ = events.send(ui::UIEvent::Disconnected).await; let (mut child, socks_port) = - spawn_ssh(remote).await.expect("failed to spawn"); + spawn_ssh(remote, sudo).await.expect("failed to spawn"); let mut stderr = child .stderr @@ -377,7 +384,7 @@ async fn client_connect_loop(remote: &str, events: mpsc::Sender) { } } -pub async fn run_client(remote: &str) { +pub async fn run_client(remote: &str, sudo: bool) { let (event_sender, event_receiver) = mpsc::channel(1024); _ = log::set_boxed_logger(ui::Logger::new(event_sender.clone())); log::set_max_level(LevelFilter::Info); @@ -395,7 +402,7 @@ pub async fn run_client(remote: &str) { // Start the reconnect loop. tokio::select! { _ = ui.run() => (), - _ = client_connect_loop(remote, event_sender) => () + _ = client_connect_loop(remote, sudo, event_sender) => () } } diff --git a/src/main.rs b/src/main.rs index 243f4f4..667ff1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); fn usage() { println!(indoc! {" -usage: fwd [--version] ( | browse ) +usage: fwd [options] ( | browse ) To connect a client to a server that has an `fwd` installed in its path, run `fwd ` on the client, where is the name of the server to @@ -13,6 +13,15 @@ connect to. On a server that already has a client connected to it you can use `fwd browse ` to open `` in the default browser of the client. + +Options: + --version Print the version of fwd and exit + --sudo, -s Run the server side of fwd with `sudo`. This allows the + client to forward ports that are open by processes being + run under other accounts (e.g., docker containers being + run as root), but requires sudo access on the server and + *might* end up forwarding ports that you do not want + forwarded (e.g., port 22 for sshd, or port 53 for systemd.) "}); } @@ -21,33 +30,46 @@ enum Args { Help, Version, Server, - Client(String), + Client(String, bool), Browse(String), Error, } fn parse_args(args: Vec) -> Args { - // Look for help; allow it to come anywhere because sometimes you just - // want to jam it on the end of an existing command line. - for arg in &args { + let mut server = None; + let mut sudo = None; + let mut rest = Vec::new(); + + for arg in args.into_iter().skip(1) { if arg == "--help" || arg == "-?" || arg == "-h" { return Args::Help; + } else if arg == "--version" { + return Args::Version; + } else if arg == "--server" { + server = Some(true) + } else if arg == "--sudo" || arg == "-s" { + sudo = Some(true) + } else { + rest.push(arg) } } - // No help, parse for reals. - if args.len() >= 2 && args[1] == "--version" { - Args::Version - } else if args.len() == 2 && &args[1] == "--server" { - Args::Server - } else if args.len() == 3 && args[1] == "browse" { - Args::Browse(args[2].to_string()) - } else { - if args.len() != 2 { - Args::Error + if server.unwrap_or(false) { + if rest.len() == 0 && sudo.is_none() { + Args::Server } else { - Args::Client(args[1].to_string()) + Args::Error } + } else if rest.len() > 1 && rest[0] == "browse" { + if rest.len() == 2 { + Args::Browse(rest[1].to_string()) + } else { + Args::Error + } + } else if rest.len() == 1 { + Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + } else { + Args::Error } } @@ -66,8 +88,8 @@ async fn main() { Args::Browse(url) => { fwd::browse_url(&url).await; } - Args::Client(server) => { - fwd::run_client(&server).await; + Args::Client(server, sudo) => { + fwd::run_client(&server, sudo).await; } Args::Error => { usage(); @@ -111,13 +133,18 @@ mod tests { assert_arg_parse!(&["browse", "google.com", "what"], Args::Error); assert_arg_parse!(&["a", "b"], Args::Error); assert_arg_parse!(&["--server", "something"], Args::Error); + assert_arg_parse!(&["--server", "--sudo"], Args::Error); + assert_arg_parse!(&["--server", "-s"], Args::Error); } #[test] fn client() { - assert_arg_parse!(&["foo.com"], Args::Client(_)); - assert_arg_parse!(&["a"], Args::Client(_)); - assert_arg_parse!(&["browse"], Args::Client(_)); + assert_arg_parse!(&["foo.com"], Args::Client(_, false)); + assert_arg_parse!(&["a"], Args::Client(_, false)); + assert_arg_parse!(&["browse"], Args::Client(_, false)); + assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true)); + assert_arg_parse!(&["a", "-s"], Args::Client(_, true)); + assert_arg_parse!(&["-s", "browse"], Args::Client(_, true)); } #[test] From 815ee5e86ea6c137b6e78b520a73408519ce65ee Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 30 Apr 2023 07:22:27 -0700 Subject: [PATCH 02/85] Remove explanation for what fwd does You probably know already, and it feels clunky here. --- src/client/ui.rs | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 5732833..648880a 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -22,8 +22,7 @@ use tui::{ layout::{Constraint, Direction, Layout, Margin, Rect}, style::{Color, Style}, widgets::{ - Block, Borders, Clear, List, ListItem, ListState, Paragraph, Row, - Table, TableState, + Block, Borders, List, ListItem, ListState, Row, Table, TableState, }, Frame, Terminal, }; @@ -302,12 +301,11 @@ impl UI { Row::new(vec!["l", "Show fwd's logs"]), ]; - let help_intro = 4; let border_lines = 3; let help_popup_area = centered_rect( 65, - keybindings.len() as u16 + help_intro + border_lines, + keybindings.len() as u16 + border_lines, frame.size(), ); let inner_area = @@ -315,15 +313,6 @@ impl UI { let key_width = 7; let binding_width = inner_area.width.saturating_sub(key_width); - let constraints = vec![ - Constraint::Length(help_intro), - Constraint::Length(inner_area.height - help_intro), - ]; - let help_parts = Layout::default() - .direction(Direction::Vertical) - .constraints(constraints) - .split(inner_area); - let keybindings_widths = &[ Constraint::Length(key_width), Constraint::Length(binding_width), @@ -331,22 +320,10 @@ impl UI { let keybindings = Table::new(keybindings) .widths(keybindings_widths) .column_spacing(1) - .block(Block::default().title("Keys").borders(Borders::TOP)); - - let exp = Paragraph::new( - "fwd automatically listens for connections on the same ports\nas the target, and forwards connections on those ports to the\ntarget.", - ); - - // outer box - frame.render_widget(Clear, help_popup_area); //this clears out the background - let helpbox = Block::default().title("Help").borders(Borders::ALL); - frame.render_widget(helpbox, help_popup_area); - - // explanation - frame.render_widget(exp, help_parts[0]); + .block(Block::default().title("Keys").borders(Borders::ALL)); // keybindings - frame.render_widget(keybindings, help_parts[1]); + frame.render_widget(keybindings, inner_area); } fn render_logs(&mut self, frame: &mut Frame, size: Rect) { From b85e3fa9a6040151d94011e92634033ba7ad64be Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 30 Apr 2023 07:33:06 -0700 Subject: [PATCH 03/85] Fix crash on changing selection with no ports No ports always means no selection --- src/client/ui.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 648880a..efb0ca3 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -471,25 +471,37 @@ impl UI { Some(i) => { assert!(self.ports.len() > 0, "We must have ports because we have a selection."); if i == 0 { - self.ports.len() - 1 + Some(self.ports.len() - 1) } else { - i - 1 + Some(i - 1) + } + } + None => { + if self.ports.len() > 0 { + Some(0) + } else { + None } } - None => 0, }; - self.selection.select(Some(index)); + self.selection.select(index); } KeyEvent { code: KeyCode::Down, .. } | KeyEvent { code: KeyCode::Char('j'), .. } => { let index = match self.selection.selected() { Some(i) => { assert!(self.ports.len() > 0, "We must have ports because we have a selection."); - (i + 1) % self.ports.len() + Some((i + 1) % self.ports.len()) + } + None => { + if self.ports.len() > 0 { + Some(0) + } else { + None + } } - None => 0, }; - self.selection.select(Some(index)); + self.selection.select(index); } KeyEvent { code: KeyCode::Enter, .. } => { if let Some(p) = self.get_selected_port() { From fd02779ba08124814b65d71c0efdc8e31fcdd7a8 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 30 Apr 2023 07:33:55 -0700 Subject: [PATCH 04/85] Bump version to 0.7.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f5f475..1a3619a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,7 +229,7 @@ checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "fwd" -version = "0.6.2" +version = "0.7.0" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index 6893ca2..3b6dc94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.6.2" +version = "0.7.0" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" From 9671da9750edef4a0cc09c3768a296cb0246d378 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 28 Aug 2023 09:25:46 -0700 Subject: [PATCH 05/85] Make notes about the future --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index fc074c7..1e3c07d 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,9 @@ If the port is something that might be interesting to a web browser, you can pre If something is going wrong, pressing `l` will toggle logs that might explain it. Press `q` to quit. + +## Future Improvements: + +- Clipboard integration: send something from the remote end of the pipe to the host's clipboard. (Sometimes you *really* want to copy some big buffer from the remote side and your terminal just can't make that work.) + +- Client heartbeats: I frequently wind up in a situation where the pipe is stalled: not broken but nothing is getting through. (This happens with my coder.com pipes all the time.) From 519b7bc415a9ea189e4e0fd423971f91cef39f19 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 25 Nov 2023 07:33:44 -0800 Subject: [PATCH 06/85] Display the URL we're trying to open when it fails This means that it is not lost forever and you can, I don't know, click it in your terminal window or something. (Thanks @quodlibetor for the patch!) --- src/browse/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browse/mod.rs b/src/browse/mod.rs index 88348d8..cf83858 100644 --- a/src/browse/mod.rs +++ b/src/browse/mod.rs @@ -11,6 +11,7 @@ use browse_unix::{browse_url_impl, handle_browser_open_impl}; #[inline] pub async fn browse_url(url: &String) { if let Err(e) = browse_url_impl(url).await { + eprintln!("Unable to open {url}"); eprintln!("{}", e); std::process::exit(1); } From 00daedeb95c73f1c48e0a63996136d3a07d8d1c0 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 25 Nov 2023 07:57:52 -0800 Subject: [PATCH 07/85] Some silly refactoring --- src/server/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/server/mod.rs b/src/server/mod.rs index 4a369e2..1b1f1b2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -14,13 +14,8 @@ async fn write_driver( messages: &mut mpsc::Receiver, writer: &mut MessageWriter, ) -> () { - loop { - match messages.recv().await { - Some(m) => { - writer.write(m).await.expect("Failed to write the message") - } - None => break, - } + while let Some(m) = messages.recv().await { + writer.write(m).await.expect("Failed to write the message") } } From 2684d7f009239953de403c610c720c79c566236c Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 25 Nov 2023 07:58:00 -0800 Subject: [PATCH 08/85] Upgrade dependencies --- Cargo.lock | 479 +++++++++++++++++++++++------------------------------ 1 file changed, 209 insertions(+), 270 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a3619a..fb471ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,27 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -19,9 +34,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "assert_matches" @@ -35,6 +50,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -43,21 +73,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cassowary" @@ -67,9 +97,12 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -79,24 +112,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", - "num-integer", "num-traits", - "winapi", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", + "windows-targets 0.48.5", ] [[package]] @@ -133,83 +156,28 @@ dependencies = [ [[package]] name = "crossterm_winapi" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.15", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - [[package]] name = "errno" -version = "0.3.1" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ - "errno-dragonfly", "libc", "windows-sys 0.48.0", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -223,9 +191,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "fwd" @@ -251,19 +219,16 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "gimli" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -282,26 +247,25 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -312,20 +276,20 @@ checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -338,18 +302,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linux-raw-sys" @@ -359,9 +314,9 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -369,68 +324,70 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.45.0", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", + "windows-sys 0.48.0", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] -name = "once_cell" -version = "1.17.1" +name = "object" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "open" @@ -454,15 +411,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.5", ] [[package]] @@ -473,15 +430,15 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -503,9 +460,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -549,9 +506,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags", ] @@ -566,10 +523,16 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.36.13" +name = "rustc-demangle" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a38f9520be93aba504e8ca974197f46158de5dcaa9fa04b57c57cd6a679d658" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.36.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" dependencies = [ "bitflags", "errno", @@ -581,27 +544,35 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -629,36 +600,25 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" -version = "0.4.9" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -675,42 +635,33 @@ dependencies = [ "remove_dir_all", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn", ] [[package]] name = "tokio" -version = "1.28.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -725,13 +676,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn", ] [[package]] @@ -769,9 +720,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" @@ -781,9 +732,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "users" @@ -803,9 +754,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -813,24 +764,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -838,22 +789,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "winapi" @@ -871,15 +822,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -887,12 +829,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -925,7 +867,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -945,17 +887,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -966,9 +908,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -978,9 +920,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -990,9 +932,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -1002,9 +944,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -1014,9 +956,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -1026,9 +968,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -1038,15 +980,12 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "xdg" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee" -dependencies = [ - "home", -] +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" From 0368074ea0f6833fa7e8ac599aab74072efcfc0f Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 25 Nov 2023 08:04:40 -0800 Subject: [PATCH 09/85] Explicit tokio features Somehow I thought this would make my binary smaller lol --- Cargo.lock | 1 - Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb471ef..1a5d73a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,7 +666,6 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", diff --git a/Cargo.toml b/Cargo.toml index 3b6dc94..3e5ff60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" thiserror = "1.0" -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread"] } tokio-stream = "0.1" toml = "0.5" tui = "0.19" From 10984034fa934826a8e8b8fc938d1e9fcf9b26f2 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 29 Feb 2024 13:16:16 -0800 Subject: [PATCH 10/85] Supply the error message when connect fails This might be too ugly --- src/browse/browse_unix.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/browse/browse_unix.rs b/src/browse/browse_unix.rs index 5a92b88..54cca9c 100644 --- a/src/browse/browse_unix.rs +++ b/src/browse/browse_unix.rs @@ -10,9 +10,12 @@ use xdg; pub async fn browse_url_impl(url: &String) -> Result<()> { let path = socket_path().context("Error getting socket path")?; - let stream = UnixStream::connect(&path).await.context( - "Error connecting to socket (is fwd actually connected here?)", - )?; + let stream = match UnixStream::connect(&path).await { + Ok(s) => s, + Err(e) => bail!( + "Error connecting to socket: {e} (is fwd actually connected here?)" + ), + }; let mut writer = MessageWriter::new(stream); writer .write(Message::Browse(url.clone())) From 7766feafd4e51ec7dc160242ad88c3f86fee197f Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 1 Mar 2024 06:15:56 -0800 Subject: [PATCH 11/85] Bump the version in the crate Forgot to do this on release, whoops --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a5d73a..41cec3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,7 +197,7 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "fwd" -version = "0.7.0" +version = "0.8.1" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index 3e5ff60..6cf2ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.7.0" +version = "0.8.1" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" From e11b6e025e09d94457dd48f10db4455884c74fce Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 13 Apr 2024 14:59:19 -0700 Subject: [PATCH 12/85] Maybe release for aarch64? Who can say. --- .github/workflows/release.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 247ad43..1adf856 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -49,9 +49,8 @@ jobs: PCRE2_SYS_STATIC: 1 strategy: - # just an example matrix matrix: - build: ['linux', 'macos', 'windows'] + build: ['linux', 'macos', 'arm-macos', 'windows'] include: - build: linux os: ubuntu-22.04 @@ -63,6 +62,11 @@ jobs: rust: nightly target: x86_64-apple-darwin + - build: arm-macos + os: macos-12 + rust: nightly + target: aarch64-apple-darwin + - build: windows os: windows-2022 rust: nightly @@ -90,7 +94,7 @@ jobs: run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} - name: Strip release binary (linux and macos) - if: matrix.build == 'linux' || matrix.build == 'macos' + if: matrix.build == 'linux' || matrix.build == 'macos' || matrix.build == 'arm-macos' run: | strip "target/${{ matrix.target }}/release/fwd" strip "target/${{ matrix.target }}/release/fwd-browse" From 1e33561d927b39b9c31f3a51c4821eb2cbdb775c Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 14 Apr 2024 06:51:26 -0700 Subject: [PATCH 13/85] Allow manual trigger of workflow dispatch --- .github/workflows/release.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1adf856..cbda082 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,6 +8,7 @@ name: release on: + workflow_dispatch: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" From 08a41492b8d9fbe3259f359596fa3f7c7b33718b Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 21 Jun 2024 08:26:56 -0700 Subject: [PATCH 14/85] Clippy I guess --- src/client/ui.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index efb0ca3..c6cfa9f 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -10,7 +10,6 @@ use crossterm::{ }, }; use log::{error, info, Level, Metadata, Record}; -use open; use std::collections::vec_deque::VecDeque; use std::collections::{HashMap, HashSet}; use std::io::stdout; @@ -334,7 +333,7 @@ impl UI { .block(Block::default().title("Log").borders(Borders::ALL)); let mut list_state = ListState::default(); - list_state.select(if self.lines.len() > 0 { + list_state.select(if !self.lines.is_empty() { Some(self.lines.len() - 1) } else { None @@ -344,10 +343,7 @@ impl UI { } fn connected(&self) -> bool { - match self.socks_port { - Some(_) => true, - None => false, - } + self.socks_port.is_some() } fn get_ui_ports(&self) -> Vec { @@ -469,7 +465,7 @@ impl UI { | KeyEvent { code: KeyCode::Char('k'), .. } => { let index = match self.selection.selected() { Some(i) => { - assert!(self.ports.len() > 0, "We must have ports because we have a selection."); + assert!(!self.ports.is_empty(), "We must have ports because we have a selection."); if i == 0 { Some(self.ports.len() - 1) } else { @@ -477,7 +473,7 @@ impl UI { } } None => { - if self.ports.len() > 0 { + if !self.ports.is_empty() { Some(0) } else { None @@ -490,11 +486,11 @@ impl UI { | KeyEvent { code: KeyCode::Char('j'), .. } => { let index = match self.selection.selected() { Some(i) => { - assert!(self.ports.len() > 0, "We must have ports because we have a selection."); + assert!(!self.ports.is_empty(), "We must have ports because we have a selection."); Some((i + 1) % self.ports.len()) } None => { - if self.ports.len() > 0 { + if !self.ports.is_empty() { Some(0) } else { None @@ -606,7 +602,8 @@ impl Drop for UI { } } -/// helper function to create a centered rect using up certain percentage of the available rect `r` +/// helper function to create a centered rect using up certain percentage of +/// the available rect `r` fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect { let left = r.left() + if width_chars > r.width { From 3f7afc5b78217594d6db8c0b3d78ae1af5617357 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 22 Jun 2024 06:13:09 -0700 Subject: [PATCH 15/85] More clippy --- src/server/refresh.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/server/refresh.rs b/src/server/refresh.rs index e21cf6f..a62dfa7 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -18,19 +18,15 @@ pub fn get_entries() -> Result> { // error we encounter as it probably means we have no access to that // process or something. let mut map: HashMap = HashMap::new(); - for p in all_procs { - if let Ok(process) = p { - if !process.is_alive() { - continue; // Ignore zombies. - } + for process in all_procs.flatten() { + if !process.is_alive() { + continue; // Ignore zombies. + } - if let (Ok(fds), Ok(cmd)) = (process.fd(), process.cmdline()) { - for fd in fds { - if let Ok(fd) = fd { - if let FDTarget::Socket(inode) = fd.target { - map.insert(inode, cmd.join(" ")); - } - } + if let (Ok(fds), Ok(cmd)) = (process.fd(), process.cmdline()) { + for fd in fds.flatten() { + if let FDTarget::Socket(inode) = fd.target { + map.insert(inode, cmd.join(" ")); } } } From 3eba65f6e6172b62da09744dc22e09acd5765dfd Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 22 Jun 2024 07:32:11 -0700 Subject: [PATCH 16/85] Refactor in prep for clip --- Cargo.toml | 2 +- src/bin/fwd-browse.rs | 7 +- src/browse/mod.rs | 40 --------- src/lib.rs | 4 +- src/main.rs | 22 +++-- src/reverse.rs | 29 +++++++ .../browse_unix.rs => reverse/unix.rs} | 85 ++++++++++--------- src/server/mod.rs | 6 +- 8 files changed, 101 insertions(+), 94 deletions(-) delete mode 100644 src/browse/mod.rs create mode 100644 src/reverse.rs rename src/{browse/browse_unix.rs => reverse/unix.rs} (90%) diff --git a/Cargo.toml b/Cargo.toml index 6cf2ec4..29824df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" thiserror = "1.0" -tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread"] } +tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" toml = "0.5" tui = "0.19" diff --git a/src/bin/fwd-browse.rs b/src/bin/fwd-browse.rs index bbebcdf..9899597 100644 --- a/src/bin/fwd-browse.rs +++ b/src/bin/fwd-browse.rs @@ -9,5 +9,10 @@ async fn main() { std::process::exit(1); } - fwd::browse_url(&args[1]).await; + let url = &args[1]; + if let Err(e) = fwd::browse_url(url).await { + eprintln!("Unable to open {url}"); + eprintln!("{}", e); + std::process::exit(1); + } } diff --git a/src/browse/mod.rs b/src/browse/mod.rs deleted file mode 100644 index cf83858..0000000 --- a/src/browse/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::message::Message; -use anyhow::Result; -use tokio::sync::mpsc; - -#[cfg(target_family = "unix")] -mod browse_unix; - -#[cfg(target_family = "unix")] -use browse_unix::{browse_url_impl, handle_browser_open_impl}; - -#[inline] -pub async fn browse_url(url: &String) { - if let Err(e) = browse_url_impl(url).await { - eprintln!("Unable to open {url}"); - eprintln!("{}", e); - std::process::exit(1); - } -} - -#[cfg(not(target_family = "unix"))] -pub async fn browse_url_impl(_url: &String) -> Result<()> { - use anyhow::anyhow; - Err(anyhow!( - "Opening a browser is not supported on this platform" - )) -} - -#[inline] -pub async fn handle_browser_open( - messages: mpsc::Sender, -) -> Result<()> { - handle_browser_open_impl(messages).await -} - -#[cfg(not(target_family = "unix"))] -async fn handle_browser_open_impl( - _messages: mpsc::Sender, -) -> Result<()> { - std::future::pending().await -} diff --git a/src/lib.rs b/src/lib.rs index 5eaba0d..6143ba3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ -mod browse; mod client; mod message; +mod reverse; mod server; -pub use browse::browse_url; pub use client::run_client; +pub use reverse::browse_url; pub use server::run_server; diff --git a/src/main.rs b/src/main.rs index 667ff1c..cf033ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); fn usage() { println!(indoc! {" -usage: fwd [options] ( | browse ) +usage: fwd [options] ( | browse | clip []) To connect a client to a server that has an `fwd` installed in its path, run `fwd ` on the client, where is the name of the server to @@ -60,9 +60,13 @@ fn parse_args(args: Vec) -> Args { } else { Args::Error } - } else if rest.len() > 1 && rest[0] == "browse" { - if rest.len() == 2 { - Args::Browse(rest[1].to_string()) + } else if rest.len() > 1 { + if rest[0] == "browse" { + if rest.len() == 2 { + Args::Browse(rest[1].to_string()) + } else { + Args::Error + } } else { Args::Error } @@ -73,6 +77,14 @@ fn parse_args(args: Vec) -> Args { } } +async fn browse_url(url: &str) { + if let Err(e) = fwd::browse_url(&url).await { + eprintln!("Unable to open {url}"); + eprintln!("{}", e); + std::process::exit(1); + } +} + #[tokio::main] async fn main() { match parse_args(std::env::args().collect()) { @@ -86,7 +98,7 @@ async fn main() { fwd::run_server().await; } Args::Browse(url) => { - fwd::browse_url(&url).await; + browse_url(&url).await; } Args::Client(server, sudo) => { fwd::run_client(&server, sudo).await; diff --git a/src/reverse.rs b/src/reverse.rs new file mode 100644 index 0000000..bca8c1c --- /dev/null +++ b/src/reverse.rs @@ -0,0 +1,29 @@ +use anyhow::Result; + +#[cfg(target_family = "unix")] +mod unix; + +#[cfg(target_family = "unix")] +pub use unix::{handle_reverse_connections, send_reverse_message}; + +use crate::message::Message; + +#[cfg(not(target_family = "unix"))] +pub async fn send_reverse_message(_message: Message) -> Result<()> { + use anyhow::anyhow; + Err(anyhow!( + "Server-side operations are not supported on this platform" + )) +} + +#[cfg(not(target_family = "unix"))] +pub async fn handle_reverse_connections( + _messages: mpsc::Sender, +) -> Result<()> { + std::future::pending().await +} + +#[inline] +pub async fn browse_url(url: &str) -> Result<()> { + send_reverse_message(Message::Browse(url.to_string())).await +} diff --git a/src/browse/browse_unix.rs b/src/reverse/unix.rs similarity index 90% rename from src/browse/browse_unix.rs rename to src/reverse/unix.rs index 54cca9c..92025ce 100644 --- a/src/browse/browse_unix.rs +++ b/src/reverse/unix.rs @@ -1,14 +1,15 @@ -use crate::message::{Message, MessageReader, MessageWriter}; +// The reverse client connects to the server via a local connection to send +// commands back to the client. use anyhow::{bail, Context, Result}; use log::warn; use std::os::unix::fs::DirBuilderExt; use std::path::PathBuf; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::mpsc; -use users; -use xdg; -pub async fn browse_url_impl(url: &String) -> Result<()> { +use crate::message::{Message, MessageReader, MessageWriter}; + +pub async fn send_reverse_message(message: Message) -> Result<()> { let path = socket_path().context("Error getting socket path")?; let stream = match UnixStream::connect(&path).await { Ok(s) => s, @@ -18,38 +19,22 @@ pub async fn browse_url_impl(url: &String) -> Result<()> { }; let mut writer = MessageWriter::new(stream); writer - .write(Message::Browse(url.clone())) + .write(message) .await .context("Error sending browse message")?; Ok(()) } -pub async fn handle_browser_open_impl( - messages: mpsc::Sender, -) -> Result<()> { - let path = socket_path().context("Error getting socket path")?; - handle_browser_open_with_path(messages, path).await -} - -async fn handle_browser_open_with_path( - messages: mpsc::Sender, - path: PathBuf, -) -> Result<()> { - let _ = std::fs::remove_file(&path); - let listener = UnixListener::bind(&path) - .with_context(|| format!("Failed to bind to {}", path.display()))?; - loop { - let (socket, _addr) = listener - .accept() - .await - .context("Error accepting connection")?; - - let sender = messages.clone(); - tokio::spawn(async move { - if let Err(e) = handle_connection(socket, sender).await { - warn!("Error handling socket connection: {:?}", e); - } - }); +fn socket_directory() -> Result { + let base_directories = xdg::BaseDirectories::new() + .context("Error creating BaseDirectories")?; + match base_directories.place_runtime_file("fwd") { + Ok(path) => Ok(path), + Err(_) => { + let mut path = std::env::temp_dir(); + path.push(format!("fwd{}", users::get_current_uid())); + Ok(path) + } } } @@ -68,16 +53,32 @@ pub fn socket_path() -> Result { Ok(socket_path) } -fn socket_directory() -> Result { - let base_directories = xdg::BaseDirectories::new() - .context("Error creating BaseDirectories")?; - match base_directories.place_runtime_file("fwd") { - Ok(path) => Ok(path), - Err(_) => { - let mut path = std::env::temp_dir(); - path.push(format!("fwd{}", users::get_current_uid())); - Ok(path) - } +pub async fn handle_reverse_connections( + messages: mpsc::Sender, +) -> Result<()> { + let path = socket_path().context("Error getting socket path")?; + handle_reverse_connections_with_path(messages, path).await +} + +async fn handle_reverse_connections_with_path( + messages: mpsc::Sender, + path: PathBuf, +) -> Result<()> { + let _ = std::fs::remove_file(&path); + let listener = UnixListener::bind(&path) + .with_context(|| format!("Failed to bind to {}", path.display()))?; + loop { + let (socket, _addr) = listener + .accept() + .await + .context("Error accepting connection")?; + + let sender = messages.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(socket, sender).await { + warn!("Error handling socket connection: {:?}", e); + } + }); } } @@ -119,7 +120,7 @@ mod tests { let path_override = path.clone(); tokio::spawn(async move { - handle_browser_open_with_path(sender, path_override) + handle_reverse_connections_with_path(sender, path_override) .await .expect("Error in server!"); }); diff --git a/src/server/mod.rs b/src/server/mod.rs index 1b1f1b2..86e1ef7 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,5 @@ -use crate::browse::handle_browser_open; use crate::message::{Message, MessageReader, MessageWriter}; +use crate::reverse::handle_reverse_connections; use anyhow::Result; use log::{error, warn}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; @@ -13,7 +13,7 @@ mod refresh; async fn write_driver( messages: &mut mpsc::Receiver, writer: &mut MessageWriter, -) -> () { +) { while let Some(m) = messages.recv().await { writer.write(m).await.expect("Failed to write the message") } @@ -77,7 +77,7 @@ async fn server_main< tokio::select! { _ = write_driver(&mut receiver, &mut writer) => Ok(()), r = server_loop(&mut reader, &mut sender) => r, - r = handle_browser_open(browse_sender) => r, + r = handle_reverse_connections(browse_sender) => r, } } From fb86cbd0ded1d483afd91023a963fadf7cc50a81 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 22 Jun 2024 07:32:43 -0700 Subject: [PATCH 17/85] Bump crate version for unreleased --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 29824df..6b516f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.8.1" +version = "0.9" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" From a40a493d39ffd7d1b092845758ed60cb03af1b06 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 23 Jun 2024 08:54:41 -0700 Subject: [PATCH 18/85] Initial implementation of clipboard forwarding --- Cargo.lock | 584 ++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 +- src/client/mod.rs | 87 +++++-- src/lib.rs | 1 + src/main.rs | 26 ++- src/message.rs | 71 +++++- src/reverse.rs | 47 ++++ 7 files changed, 776 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41cec3d..44eb670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "bumpalo" version = "3.14.0" @@ -89,6 +101,32 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.5.0", + "log", + "polling", + "rustix 0.38.34", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -122,6 +160,39 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -137,13 +208,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crossterm" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossterm_winapi", "futures-core", "libc", @@ -164,13 +241,34 @@ dependencies = [ ] [[package]] -name = "errno" -version = "0.3.7" +name = "cursor-icon" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -197,17 +295,19 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "fwd" -version = "0.8.1" +version = "0.9.0" dependencies = [ "anyhow", "assert_matches", "bytes", + "copypasta", "crossterm", "home", "indoc", "log", "open", "procfs", + "rand 0.8.5", "tempdir", "thiserror", "tokio", @@ -218,6 +318,27 @@ dependencies = [ "xdg", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -230,6 +351,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -280,7 +407,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", "windows-sys 0.48.0", ] @@ -294,6 +421,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -302,9 +435,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if", + "windows-targets 0.48.5", +] [[package]] name = "linux-raw-sys" @@ -312,6 +455,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.11" @@ -328,12 +477,30 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -370,10 +537,39 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "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.32.1" @@ -434,6 +630,33 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.69" @@ -449,13 +672,22 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "chrono", "flate2", "hex", "lazy_static", - "rustix", + "rustix 0.36.17", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", ] [[package]] @@ -480,6 +712,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -495,6 +748,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -510,7 +772,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -534,14 +796,33 @@ version = "0.36.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", "windows-sys 0.45.0", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -598,12 +879,57 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.5.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + [[package]] name = "socket2" version = "0.5.5" @@ -631,7 +957,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "rand", + "rand 0.4.6", "remove_dir_all", ] @@ -704,13 +1030,29 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + [[package]] name = "tui" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cassowary", "crossterm", "unicode-segmentation", @@ -805,6 +1147,102 @@ version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +[[package]] +name = "wayland-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.34", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +dependencies = [ + "bitflags 2.5.0", + "rustix 0.38.34", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "winapi" version = "0.3.9" @@ -869,6 +1307,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[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.5", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -899,6 +1346,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -911,6 +1374,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -923,6 +1392,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -935,6 +1410,18 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -947,6 +1434,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -959,6 +1452,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -971,6 +1470,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -983,8 +1488,53 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.34", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + [[package]] name = "xdg" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" diff --git a/Cargo.toml b/Cargo.toml index 6b516f2..1193872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.9" +version = "0.9.0" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" @@ -16,11 +16,13 @@ bench = false [dependencies] anyhow = "1.0" bytes = "1" +copypasta = "0.10.1" crossterm = { version = "0.25", features = ["event-stream"] } home = "0.5.4" indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" +rand = "0.8.5" thiserror = "1.0" tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" diff --git a/src/client/mod.rs b/src/client/mod.rs index 567a186..e13b779 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,8 +1,10 @@ use crate::message::{Message, MessageReader, MessageWriter}; use anyhow::{bail, Result}; use bytes::BytesMut; +use copypasta::{ClipboardContext, ClipboardProvider}; use log::LevelFilter; use log::{debug, error, info, warn}; +use std::collections::HashMap; use std::net::{Ipv4Addr, SocketAddrV4}; use tokio::io::{ AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, @@ -15,6 +17,14 @@ use tokio::sync::mpsc; mod config; mod ui; +// 256MB clipboard. Operating systems do not generally have a maximum size +// but I need to buffer it all in memory in order to use the `copypasta` +// crate and so I want to set a limit somewhere and this seems.... rational. +// You should use another transfer mechanism other than this if you need more +// than 256 MB of data. (Obviously future me will come along and shake his +// head at the limitations of my foresight, but oh well.) +const MAX_CLIPBOARD_SIZE: usize = 256 * 1024 * 1024; + /// Wait for the server to be ready; we know the server is there and /// listening when we see the special sync marker, which is 8 NUL bytes in a /// row. @@ -162,25 +172,22 @@ async fn client_handle_connection( /// Listen on a port that we are currently forwarding, and use the SOCKS5 /// proxy on the specified port to handle the connections. async fn client_listen(port: u16, socks_port: u16) -> Result<()> { + let listener = + TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)).await?; loop { - let listener = - TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)) - .await?; - loop { - // The second item contains the IP and port of the new - // connection, but we don't care. - let (socket, _) = listener.accept().await?; + // The second item contains the IP and port of the new + // connection, but we don't care. + let (socket, _) = listener.accept().await?; - tokio::spawn(async move { - if let Err(e) = - client_handle_connection(socks_port, port, socket).await - { - error!("Error handling connection: {:?}", e); - } else { - debug!("Done???"); - } - }); - } + tokio::spawn(async move { + if let Err(e) = + client_handle_connection(socks_port, port, socket).await + { + error!("Error handling connection: {:?}", e); + } else { + debug!("Done???"); + } + }); } } @@ -188,20 +195,60 @@ async fn client_handle_messages( mut reader: MessageReader, events: mpsc::Sender, ) -> Result<()> { + let mut clipboard_messages = HashMap::new(); loop { use Message::*; match reader.read().await? { Ping => (), + Ports(ports) => { - if let Err(_) = events.send(ui::UIEvent::Ports(ports)).await { - // TODO: Log + if let Err(e) = events.send(ui::UIEvent::Ports(ports)).await { + error!("Error sending ports request: {:?}", e); } } + Browse(url) => { - // TODO: Uh, security? info!("Browsing to {url}..."); _ = open::that(url); } + + ClipStart(id) => { + info!("Starting clip op {id}"); + clipboard_messages.insert(id, Vec::new()); + } + + ClipData(id, mut data) => match clipboard_messages.get_mut(&id) { + Some(bytes) => { + info!( + "Received data for clip op {id} ({len} bytes)", + len = data.len() + ); + if bytes.len() < MAX_CLIPBOARD_SIZE { + bytes.append(&mut data); + } + } + None => { + warn!("Received data for unknown clip op {id}"); + } + }, + + ClipEnd(id) => { + let Some(data) = clipboard_messages.remove(&id) else { + warn!("Received end for unknown clip op {id}"); + continue; + }; + + let Ok(data) = String::from_utf8(data) else { + warn!("Received invalid utf8 for clipboard on op {id}"); + continue; + }; + + let mut ctx = ClipboardContext::new().unwrap(); + if let Err(e) = ctx.set_contents(data) { + error!("Unable to set clipboard data for op {id}: {e:?}"); + } + } + message => error!("Unsupported: {:?}", message), }; } diff --git a/src/lib.rs b/src/lib.rs index 6143ba3..a1e99a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,5 @@ mod server; pub use client::run_client; pub use reverse::browse_url; +pub use reverse::clip_file; pub use server::run_server; diff --git a/src/main.rs b/src/main.rs index cf033ba..3e9ab16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,10 @@ connect to. On a server that already has a client connected to it you can use `fwd browse ` to open `` in the default browser of the client. +On a server that already has a client connected to it you can use `fwd clip` +to read stdin and send it to the clipboard of the client, or `fwd clip ` +to send the the contents of `file`. + Options: --version Print the version of fwd and exit --sudo, -s Run the server side of fwd with `sudo`. This allows the @@ -32,6 +36,7 @@ enum Args { Server, Client(String, bool), Browse(String), + Clip(Option), Error, } @@ -60,13 +65,21 @@ fn parse_args(args: Vec) -> Args { } else { Args::Error } - } else if rest.len() > 1 { + } else if rest.len() >= 1 { if rest[0] == "browse" { if rest.len() == 2 { Args::Browse(rest[1].to_string()) } else { Args::Error } + } else if rest[0] == "clip" { + if rest.len() == 1 { + Args::Clip(None) + } else if rest.len() == 2 { + Args::Clip(Some(rest[1].to_string())) + } else { + Args::Error + } } else { Args::Error } @@ -85,6 +98,14 @@ async fn browse_url(url: &str) { } } +async fn clip_file(file: Option) { + if let Err(e) = fwd::clip_file(file.as_deref()).await { + eprintln!("Unable to copy to the clipboard"); + eprintln!("{}", e); + std::process::exit(1); + } +} + #[tokio::main] async fn main() { match parse_args(std::env::args().collect()) { @@ -100,6 +121,9 @@ async fn main() { Args::Browse(url) => { browse_url(&url).await; } + Args::Clip(file) => { + clip_file(file).await; + } Args::Client(server, sudo) => { fwd::run_client(&server, sudo).await; } diff --git a/src/message.rs b/src/message.rs index eb5efd8..6b77615 100644 --- a/src/message.rs +++ b/src/message.rs @@ -57,6 +57,11 @@ pub enum Message { // Browse a thing Browse(String), + + // Send data to the remote clipboard + ClipStart(u64), + ClipData(u64, Vec), + ClipEnd(u64), } impl Message { @@ -103,6 +108,19 @@ impl Message { result.put_u8(0x07); put_string(result, url); } + ClipStart(id) => { + result.put_u8(0x08); + result.put_u64(*id); + } + ClipData(id, data) => { + result.put_u8(0x09); + result.put_u64(*id); + put_data(result, data); + } + ClipEnd(id) => { + result.put_u8(0x0A); + result.put_u64(*id); + } }; } @@ -132,7 +150,20 @@ impl Message { Ok(Ports(ports)) } 0x07 => Ok(Browse(get_string(cursor)?)), - b => Err(Error::Unknown(b).into()), + 0x08 => { + let id = get_u64(cursor)?; + Ok(ClipStart(id)) + } + 0x09 => { + let id = get_u64(cursor)?; + let data = get_data(cursor)?; + Ok(Self::ClipData(id, data)) + } + 0x0A => { + let id = get_u64(cursor)?; + Ok(ClipEnd(id)) + } + b => Err(Error::Unknown(b)), } } } @@ -151,6 +182,13 @@ fn get_u16(cursor: &mut Cursor<&[u8]>) -> Result { Ok(cursor.get_u16()) } +fn get_u64(cursor: &mut Cursor<&[u8]>) -> Result { + if cursor.remaining() < 8 { + return Err(Error::Incomplete); + } + Ok(cursor.get_u64()) +} + fn get_bytes(cursor: &mut Cursor<&[u8]>, length: usize) -> Result { if cursor.remaining() < length { return Err(Error::Incomplete); @@ -182,6 +220,22 @@ fn put_string(target: &mut T, str: &str) { target.put_slice(str.as_bytes()); } +fn put_data(target: &mut T, data: &[u8]) { + target.put_u16(data.len().try_into().expect("Buffer is too long")); + target.put_slice(data); +} + +fn get_data(cursor: &mut Cursor<&[u8]>) -> Result> { + let length = get_u16(cursor)?; + if cursor.remaining() < length.into() { + return Err(Error::Incomplete); + } + + let mut data: Vec = vec![0; length.into()]; + cursor.copy_to_slice(&mut data); + Ok(data) +} + // ---------------------------------------------------------------------------- // Message IO @@ -193,14 +247,14 @@ impl MessageWriter { pub fn new(writer: T) -> MessageWriter { MessageWriter { writer } } - pub async fn write(self: &mut Self, msg: Message) -> Result<()> { + pub async fn write(&mut self, msg: Message) -> Result<()> { // TODO: Optimize buffer usage please this is bad // eprintln!("? {:?}", msg); - let mut buffer = msg.encode(); + let buffer = msg.encode(); self.writer .write_u32(buffer.len().try_into().expect("Message too large")) .await?; - self.writer.write_all(&mut buffer).await?; + self.writer.write_all(&buffer).await?; self.writer.flush().await?; Ok(()) } @@ -214,7 +268,8 @@ impl MessageReader { pub fn new(reader: T) -> MessageReader { MessageReader { reader } } - pub async fn read(self: &mut Self) -> Result { + + pub async fn read(&mut self) -> Result { let frame_length = self.reader.read_u32().await?; let mut data = vec![0; frame_length.try_into().unwrap()]; self.reader.read_exact(&mut data).await?; @@ -283,6 +338,12 @@ mod message_tests { }, ])); assert_round_trip(Browse("https://google.com/".to_string())); + assert_round_trip(ClipStart(0x1234567890ABCDEF)); + assert_round_trip(ClipData( + 0x1234567890ABCDEF, + vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + )); + assert_round_trip(ClipEnd(0x1234567890ABCDEF)); } #[test] diff --git a/src/reverse.rs b/src/reverse.rs index bca8c1c..8e0b8ed 100644 --- a/src/reverse.rs +++ b/src/reverse.rs @@ -1,4 +1,6 @@ use anyhow::Result; +use rand::random; +use tokio::io::{AsyncRead, AsyncReadExt}; #[cfg(target_family = "unix")] mod unix; @@ -27,3 +29,48 @@ pub async fn handle_reverse_connections( pub async fn browse_url(url: &str) -> Result<()> { send_reverse_message(Message::Browse(url.to_string())).await } + +async fn clip_reader(reader: &mut T) -> Result<()> { + let clip_id: u64 = random(); + send_reverse_message(Message::ClipStart(clip_id)).await?; + + let mut count = 0; + let mut buf = vec![0; 1024]; + loop { + let read = reader.read(&mut buf[count..]).await?; + if read == 0 { + break; + } + count += read; + if count == buf.len() { + send_reverse_message(Message::ClipData(clip_id, buf)).await?; + buf = vec![0; 1024]; + count = 0; + } + } + + if count > 0 { + buf.resize(count, 0); + send_reverse_message(Message::ClipData(clip_id, buf)).await?; + } + + send_reverse_message(Message::ClipEnd(clip_id)).await?; + Ok(()) +} + +#[inline] +pub async fn clip_file(file: Option<&str>) -> Result<()> { + // send_reverse_message(Message::Browse(url.to_string())).await + match file { + Some(path) => { + let mut file = tokio::fs::File::open(path).await?; + clip_reader(&mut file).await?; + } + None => { + let mut stdin = tokio::io::stdin(); + clip_reader(&mut stdin).await?; + } + } + + Ok(()) +} From 3cb40bc2f4d46ff2b5874b7eb6e4f40db9a3abf3 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 31 Jul 2024 14:45:20 -0700 Subject: [PATCH 19/85] Clippy --- src/main.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3e9ab16..b433022 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,12 +60,12 @@ fn parse_args(args: Vec) -> Args { } if server.unwrap_or(false) { - if rest.len() == 0 && sudo.is_none() { + if rest.is_empty() && sudo.is_none() { Args::Server } else { Args::Error } - } else if rest.len() >= 1 { + } else if !rest.is_empty() { if rest[0] == "browse" { if rest.len() == 2 { Args::Browse(rest[1].to_string()) @@ -91,7 +91,7 @@ fn parse_args(args: Vec) -> Args { } async fn browse_url(url: &str) { - if let Err(e) = fwd::browse_url(&url).await { + if let Err(e) = fwd::browse_url(url).await { eprintln!("Unable to open {url}"); eprintln!("{}", e); std::process::exit(1); @@ -141,8 +141,7 @@ mod tests { // Goldarn it. fn args(x: &[&str]) -> Vec { - let mut vec: Vec = - x.into_iter().map(|a| a.to_string()).collect(); + let mut vec: Vec = x.iter().map(|a| a.to_string()).collect(); vec.insert(0, "fwd".to_string()); vec } From 604f31d8e68f9c6ee3f9a286d0cf7e9074cd4f22 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 31 Jul 2024 14:56:50 -0700 Subject: [PATCH 20/85] Fix argument parsing (whoops) This whole command line thing is actually busted; probably should make it a little bit more robust. --- src/main.rs | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index b433022..70008f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,21 +65,21 @@ fn parse_args(args: Vec) -> Args { } else { Args::Error } - } else if !rest.is_empty() { - if rest[0] == "browse" { - if rest.len() == 2 { - Args::Browse(rest[1].to_string()) - } else { - Args::Error - } - } else if rest[0] == "clip" { - if rest.len() == 1 { - Args::Clip(None) - } else if rest.len() == 2 { - Args::Clip(Some(rest[1].to_string())) - } else { - Args::Error - } + } else if rest.is_empty() { + Args::Error + } else if rest[0] == "browse" { + if rest.len() == 2 { + Args::Browse(rest[1].to_string()) + } else if rest.len() == 1 { + Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + } else { + Args::Error + } + } else if rest[0] == "clip" { + if rest.len() == 1 { + Args::Clip(None) + } else if rest.len() == 2 { + Args::Clip(Some(rest[1].to_string())) } else { Args::Error } @@ -187,6 +187,11 @@ mod tests { assert_arg_parse!(&["--server"], Args::Server); } + #[test] + fn clip() { + assert_arg_parse!(&["clip"], Args::Clip(None)); + } + #[test] fn browse() { assert_arg_parse!(&["browse", "google.com"], Args::Browse(_)); From 46bd840bc089d3f2897201f368c6082a349a1774 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 31 Jul 2024 14:58:11 -0700 Subject: [PATCH 21/85] Another test for clip garbage --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 70008f4..9988c13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -190,6 +190,7 @@ mod tests { #[test] fn clip() { assert_arg_parse!(&["clip"], Args::Clip(None)); + assert_arg_parse!(&["clip", "garbage"], Args::Clip(Some(_))); } #[test] From 3b1847d8820d73c1f80defadbbcd5acc2a999c48 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 31 Jul 2024 15:06:20 -0700 Subject: [PATCH 22/85] Require a file name for 'clip', use '-' for stdin This makes argument parsing more reliable: to `fwd` to a server named `clip` just leave off the file name. --- src/main.rs | 18 ++++++++++-------- src/reverse.rs | 18 +++++++----------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9988c13..6bd877e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ connect to. On a server that already has a client connected to it you can use `fwd browse ` to open `` in the default browser of the client. -On a server that already has a client connected to it you can use `fwd clip` +On a server that already has a client connected to it you can use `fwd clip -` to read stdin and send it to the clipboard of the client, or `fwd clip ` to send the the contents of `file`. @@ -36,7 +36,7 @@ enum Args { Server, Client(String, bool), Browse(String), - Clip(Option), + Clip(String), Error, } @@ -77,9 +77,9 @@ fn parse_args(args: Vec) -> Args { } } else if rest[0] == "clip" { if rest.len() == 1 { - Args::Clip(None) + Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) } else if rest.len() == 2 { - Args::Clip(Some(rest[1].to_string())) + Args::Clip(rest[1].to_string()) } else { Args::Error } @@ -98,8 +98,8 @@ async fn browse_url(url: &str) { } } -async fn clip_file(file: Option) { - if let Err(e) = fwd::clip_file(file.as_deref()).await { +async fn clip_file(file: String) { + if let Err(e) = fwd::clip_file(&file).await { eprintln!("Unable to copy to the clipboard"); eprintln!("{}", e); std::process::exit(1); @@ -177,9 +177,11 @@ mod tests { assert_arg_parse!(&["foo.com"], Args::Client(_, false)); assert_arg_parse!(&["a"], Args::Client(_, false)); assert_arg_parse!(&["browse"], Args::Client(_, false)); + assert_arg_parse!(&["clip"], Args::Client(_, false)); assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true)); assert_arg_parse!(&["a", "-s"], Args::Client(_, true)); assert_arg_parse!(&["-s", "browse"], Args::Client(_, true)); + assert_arg_parse!(&["-s", "clip"], Args::Client(_, true)); } #[test] @@ -189,8 +191,8 @@ mod tests { #[test] fn clip() { - assert_arg_parse!(&["clip"], Args::Clip(None)); - assert_arg_parse!(&["clip", "garbage"], Args::Clip(Some(_))); + assert_arg_parse!(&["clip", "garbage"], Args::Clip(_)); + assert_arg_parse!(&["clip", "-"], Args::Clip(_)); } #[test] diff --git a/src/reverse.rs b/src/reverse.rs index 8e0b8ed..76a89d4 100644 --- a/src/reverse.rs +++ b/src/reverse.rs @@ -59,17 +59,13 @@ async fn clip_reader(reader: &mut T) -> Result<()> { } #[inline] -pub async fn clip_file(file: Option<&str>) -> Result<()> { - // send_reverse_message(Message::Browse(url.to_string())).await - match file { - Some(path) => { - let mut file = tokio::fs::File::open(path).await?; - clip_reader(&mut file).await?; - } - None => { - let mut stdin = tokio::io::stdin(); - clip_reader(&mut stdin).await?; - } +pub async fn clip_file(file: &str) -> Result<()> { + if file == "-" { + let mut stdin = tokio::io::stdin(); + clip_reader(&mut stdin).await?; + } else { + let mut file = tokio::fs::File::open(file).await?; + clip_reader(&mut file).await?; } Ok(()) From 633594459145053bdd72f3771f558e4089833cf1 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 31 Jul 2024 15:07:27 -0700 Subject: [PATCH 23/85] One more test --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 6bd877e..004739d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -166,6 +166,7 @@ mod tests { fn errors() { assert_arg_parse!(&[], Args::Error); assert_arg_parse!(&["browse", "google.com", "what"], Args::Error); + assert_arg_parse!(&["clip", "a.txt", "b.txt"], Args::Error); assert_arg_parse!(&["a", "b"], Args::Error); assert_arg_parse!(&["--server", "something"], Args::Error); assert_arg_parse!(&["--server", "--sudo"], Args::Error); From 75343dbea2c86b457caaf14138433544c5adacef Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 4 Aug 2024 08:11:00 -0700 Subject: [PATCH 24/85] Fix build break for windows (oops) Really need to have better cross-building infrastructure for my own personal garbage. --- src/reverse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reverse.rs b/src/reverse.rs index 76a89d4..a7d8534 100644 --- a/src/reverse.rs +++ b/src/reverse.rs @@ -20,7 +20,7 @@ pub async fn send_reverse_message(_message: Message) -> Result<()> { #[cfg(not(target_family = "unix"))] pub async fn handle_reverse_connections( - _messages: mpsc::Sender, + _messages: tokio::sync::mpsc::Sender, ) -> Result<()> { std::future::pending().await } From c2c57289cff695bfb68fdb9dbbcba195860b8a35 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 4 Aug 2024 08:14:19 -0700 Subject: [PATCH 25/85] Refactor refresh Moving different mechanisms into different conditionally-compiled modules. This way it can be extended, e.g. with docker lookups, MacOS support, etc. --- src/server/refresh.rs | 65 +++++++++++------------------------- src/server/refresh/procfs.rs | 54 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 src/server/refresh/procfs.rs diff --git a/src/server/refresh.rs b/src/server/refresh.rs index a62dfa7..b1b95b8 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -1,61 +1,34 @@ -use crate::message::PortDesc; use anyhow::Result; +use log::{error, warn}; +use std::collections::HashMap; -#[cfg(not(target_os = "linux"))] -pub fn get_entries() -> Result> { - use anyhow::bail; - bail!("Not supported on this operating system"); -} +use crate::message::PortDesc; #[cfg(target_os = "linux")] +mod procfs; + pub fn get_entries() -> Result> { - use procfs::process::FDTarget; - use std::collections::HashMap; + #[allow(unused)] + let mut attempts = 0; - let all_procs = procfs::process::all_processes()?; + let mut result: HashMap = HashMap::new(); - // build up a map between socket inodes and process stat info. Ignore any - // error we encounter as it probably means we have no access to that - // process or something. - let mut map: HashMap = HashMap::new(); - for process in all_procs.flatten() { - if !process.is_alive() { - continue; // Ignore zombies. - } - - if let (Ok(fds), Ok(cmd)) = (process.fd(), process.cmdline()) { - for fd in fds.flatten() { - if let FDTarget::Socket(inode) = fd.target { - map.insert(inode, cmd.join(" ")); + #[cfg(target_os = "linux")] + { + attempts += 1; + match procfs::get_entries() { + Ok(m) => { + for (p, d) in m { + result.entry(p).or_insert(d); } } + Err(e) => error!("Error reading procfs: {e:?}"), } } - let mut h: HashMap = HashMap::new(); - - // Go through all the listening IPv4 and IPv6 sockets and take the first - // instance of listening on each port *if* the address is loopback or - // unspecified. (TODO: Do we want this restriction really?) - let tcp = procfs::net::tcp()?; - let tcp6 = procfs::net::tcp6()?; - for tcp_entry in tcp.into_iter().chain(tcp6) { - if tcp_entry.state == procfs::net::TcpState::Listen - && (tcp_entry.local_address.ip().is_loopback() - || tcp_entry.local_address.ip().is_unspecified()) - && !h.contains_key(&tcp_entry.local_address.port()) - { - if let Some(cmd) = map.get(&tcp_entry.inode) { - h.insert( - tcp_entry.local_address.port(), - PortDesc { - port: tcp_entry.local_address.port(), - desc: cmd.clone(), - }, - ); - } - } + if attempts == 0 { + warn!("Port scanning is not supported for this server"); } - Ok(h.into_values().collect()) + Ok(result.into_values().collect()) } diff --git a/src/server/refresh/procfs.rs b/src/server/refresh/procfs.rs new file mode 100644 index 0000000..aa1a79f --- /dev/null +++ b/src/server/refresh/procfs.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use procfs::process::FDTarget; +use std::collections::HashMap; + +use crate::message::PortDesc; + +pub fn get_entries() -> Result> { + let all_procs = procfs::process::all_processes()?; + + // build up a map between socket inodes and process stat info. Ignore any + // error we encounter as it probably means we have no access to that + // process or something. + let mut map: HashMap = HashMap::new(); + for process in all_procs.flatten() { + if !process.is_alive() { + continue; // Ignore zombies. + } + + if let (Ok(fds), Ok(cmd)) = (process.fd(), process.cmdline()) { + for fd in fds.flatten() { + if let FDTarget::Socket(inode) = fd.target { + map.insert(inode, cmd.join(" ")); + } + } + } + } + + let mut h: HashMap = HashMap::new(); + + // Go through all the listening IPv4 and IPv6 sockets and take the first + // instance of listening on each port *if* the address is loopback or + // unspecified. (TODO: Do we want this restriction really?) + let tcp = procfs::net::tcp()?; + let tcp6 = procfs::net::tcp6()?; + for tcp_entry in tcp.into_iter().chain(tcp6) { + if tcp_entry.state == procfs::net::TcpState::Listen + && (tcp_entry.local_address.ip().is_loopback() + || tcp_entry.local_address.ip().is_unspecified()) + && !h.contains_key(&tcp_entry.local_address.port()) + { + if let Some(cmd) = map.get(&tcp_entry.inode) { + h.insert( + tcp_entry.local_address.port(), + PortDesc { + port: tcp_entry.local_address.port(), + desc: cmd.clone(), + }, + ); + } + } + } + + Ok(h) +} From 8135f163f261cbe7a89764d8ff0a43a38eaca688 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 4 Aug 2024 08:29:33 -0700 Subject: [PATCH 26/85] Refresh is async Now we are ready for more asyncery (e.g., docker) --- src/server/mod.rs | 2 +- src/server/refresh.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/mod.rs b/src/server/mod.rs index 86e1ef7..de61f52 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -32,7 +32,7 @@ async fn server_loop( match reader.read().await? { Ping => (), Refresh => { - let ports = match refresh::get_entries() { + let ports = match refresh::get_entries().await { Ok(ports) => ports, Err(e) => { error!("Error scanning: {:?}", e); diff --git a/src/server/refresh.rs b/src/server/refresh.rs index b1b95b8..d269be0 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -7,7 +7,7 @@ use crate::message::PortDesc; #[cfg(target_os = "linux")] mod procfs; -pub fn get_entries() -> Result> { +pub async fn get_entries() -> Result> { #[allow(unused)] let mut attempts = 0; From 18da61ed320966b1332e64037fc88b4073b4a8a5 Mon Sep 17 00:00:00 2001 From: Brandon W Maister Date: Wed, 31 Jul 2024 10:43:02 -0400 Subject: [PATCH 27/85] make server logging show messages in the frontend now you can use the log crate and get messages in the frontend. --- Cargo.lock | 26 +++++++++++++++--- Cargo.toml | 1 + src/client/mod.rs | 15 +++++++---- src/main.rs | 63 ++++++++++++++++++++++++++++++++----------- src/server/mod.rs | 4 +++ src/server/refresh.rs | 7 +++-- 6 files changed, 91 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44eb670..0e3a387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,25 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "errno" version = "0.3.9" @@ -302,6 +321,7 @@ dependencies = [ "bytes", "copypasta", "crossterm", + "env_logger", "home", "indoc", "log", @@ -446,7 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -473,9 +493,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "malloc_buf" diff --git a/Cargo.toml b/Cargo.toml index 1193872..6569ef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ anyhow = "1.0" bytes = "1" copypasta = "0.10.1" crossterm = { version = "0.25", features = ["event-stream"] } +env_logger = { version = "0.11.5", default-features = false } home = "0.5.4" indoc = "1" log = { version = "0.4", features = ["std"] } diff --git a/src/client/mod.rs b/src/client/mod.rs index e13b779..79b4b15 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -323,6 +323,7 @@ async fn client_main( async fn spawn_ssh( server: &str, sudo: bool, + log_filter: &str, ) -> Result<(tokio::process::Child, u16), std::io::Error> { let socks_port = { let listener = TcpListener::bind("127.0.0.1:0").await?; @@ -337,7 +338,9 @@ async fn spawn_ssh( if sudo { cmd.arg("sudo"); } - cmd.arg("fwd").arg("--server"); + cmd.arg(format!("FWD_LOG={log_filter}")) + .arg("fwd") + .arg("--server"); cmd.stdout(std::process::Stdio::piped()); cmd.stdin(std::process::Stdio::piped()); @@ -367,13 +370,15 @@ fn is_sigint(status: std::process::ExitStatus) -> bool { async fn client_connect_loop( remote: &str, sudo: bool, + log_filter: &str, events: mpsc::Sender, ) { loop { _ = events.send(ui::UIEvent::Disconnected).await; - let (mut child, socks_port) = - spawn_ssh(remote, sudo).await.expect("failed to spawn"); + let (mut child, socks_port) = spawn_ssh(remote, sudo, log_filter) + .await + .expect("failed to spawn"); let mut stderr = child .stderr @@ -431,7 +436,7 @@ async fn client_connect_loop( } } -pub async fn run_client(remote: &str, sudo: bool) { +pub async fn run_client(remote: &str, sudo: bool, log_filter: &str) { let (event_sender, event_receiver) = mpsc::channel(1024); _ = log::set_boxed_logger(ui::Logger::new(event_sender.clone())); log::set_max_level(LevelFilter::Info); @@ -449,7 +454,7 @@ pub async fn run_client(remote: &str, sudo: bool) { // Start the reconnect loop. tokio::select! { _ = ui.run() => (), - _ = client_connect_loop(remote, sudo, event_sender) => () + _ = client_connect_loop(remote, sudo, log_filter, event_sender) => () } } diff --git a/src/main.rs b/src/main.rs index 004739d..453c78d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,10 @@ Options: run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) + --log-filter FILTER + Set remote server's log level. Default is `warn`. Supports + all of Rust's env_logger filter syntax, e.g. + `--log-filter=fwd::trace`. "}); } @@ -34,7 +38,7 @@ enum Args { Help, Version, Server, - Client(String, bool), + Client(String, bool, String), Browse(String), Clip(String), Error, @@ -43,9 +47,11 @@ enum Args { fn parse_args(args: Vec) -> Args { let mut server = None; let mut sudo = None; + let mut log_filter = None; let mut rest = Vec::new(); - for arg in args.into_iter().skip(1) { + let mut arg_iter = args.into_iter().skip(1); + while let Some(arg) = arg_iter.next() { if arg == "--help" || arg == "-?" || arg == "-h" { return Args::Help; } else if arg == "--version" { @@ -54,6 +60,14 @@ fn parse_args(args: Vec) -> Args { server = Some(true) } else if arg == "--sudo" || arg == "-s" { sudo = Some(true) + } else if arg.starts_with("--log-filter") { + if arg.contains('=') { + log_filter = Some(arg.split('=').nth(1).unwrap().to_owned()); + } else if let Some(arg) = arg_iter.next() { + log_filter = Some(arg); + } else { + return Args::Error; + } } else { rest.push(arg) } @@ -71,20 +85,24 @@ fn parse_args(args: Vec) -> Args { if rest.len() == 2 { Args::Browse(rest[1].to_string()) } else if rest.len() == 1 { - Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + Args::Client(rest[0].to_string(), sudo.unwrap_or(false), log_filter.unwrap_or("warn".to_owned())) } else { Args::Error } } else if rest[0] == "clip" { if rest.len() == 1 { - Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + Args::Client(rest[0].to_string(), sudo.unwrap_or(false), log_filter.unwrap_or("warn".to_owned())) } else if rest.len() == 2 { Args::Clip(rest[1].to_string()) } else { Args::Error } } else if rest.len() == 1 { - Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + Args::Client( + rest[0].to_string(), + sudo.unwrap_or(false), + log_filter.unwrap_or("warn".to_owned()), + ) } else { Args::Error } @@ -124,8 +142,8 @@ async fn main() { Args::Clip(file) => { clip_file(file).await; } - Args::Client(server, sudo) => { - fwd::run_client(&server, sudo).await; + Args::Client(server, sudo, log_filter) => { + fwd::run_client(&server, sudo, &log_filter).await; } Args::Error => { usage(); @@ -175,14 +193,29 @@ mod tests { #[test] fn client() { - assert_arg_parse!(&["foo.com"], Args::Client(_, false)); - assert_arg_parse!(&["a"], Args::Client(_, false)); - assert_arg_parse!(&["browse"], Args::Client(_, false)); - assert_arg_parse!(&["clip"], Args::Client(_, false)); - assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true)); - assert_arg_parse!(&["a", "-s"], Args::Client(_, true)); - assert_arg_parse!(&["-s", "browse"], Args::Client(_, true)); - assert_arg_parse!(&["-s", "clip"], Args::Client(_, true)); + assert_arg_parse!(&["foo.com"], Args::Client( _, false, _)); + assert_arg_parse!(&["a"], Args::Client(_, false, _)); + assert_arg_parse!(&["browse"], Args::Client(_, false, _)); + assert_arg_parse!(&["clip"], Args::Client(_, false, _)); + assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true, _)); + assert_arg_parse!(&["a", "-s"], Args::Client(_, true, _)); + assert_arg_parse!(&["-s", "browse"], Args::Client(_, true, _)); + assert_arg_parse!(&["-s", "clip"], Args::Client(_, true, _)); + + assert_client_parse(&["a"], "a", false, "warn"); + assert_client_parse(&["a", "--log-filter", "info"], "a", false, "info"); + assert_client_parse(&["a", "--log-filter=info"], "a", false, "info"); + assert_client_parse(&["a", "--sudo", "--log-filter=info"], "a", true, "info"); + } + + fn assert_client_parse(x: &[&str], server: &str, sudo: bool, log_filter: &str) { + let args = parse_args(args(x)); + assert_matches!(args, Args::Client(_, _, _)); + if let Args::Client(s, sdo, lf) = args { + assert_eq!(s, server); + assert_eq!(sdo, sudo); + assert_eq!(lf, log_filter); + } } #[test] diff --git a/src/server/mod.rs b/src/server/mod.rs index de61f52..1188c98 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -82,6 +82,10 @@ async fn server_main< } pub async fn run_server() { + env_logger::Builder::from_env( + env_logger::Env::new().filter_or("FWD_LOG", "warn"), + ) + .init(); let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); if let Err(e) = server_main(stdin, stdout).await { diff --git a/src/server/refresh.rs b/src/server/refresh.rs index d269be0..6ea5c93 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -1,5 +1,7 @@ use anyhow::Result; -use log::{error, warn}; +#[cfg_attr(not(target_os = "linux"), allow(unused))] +use log::error; +use log::warn; use std::collections::HashMap; use crate::message::PortDesc; @@ -8,9 +10,10 @@ use crate::message::PortDesc; mod procfs; pub async fn get_entries() -> Result> { - #[allow(unused)] + #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] let mut attempts = 0; + #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] let mut result: HashMap = HashMap::new(); #[cfg(target_os = "linux")] From 5e96b37f5b298a3f75989aa3ee14a59368faf5c8 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 5 Aug 2024 12:04:26 -0700 Subject: [PATCH 28/85] Docker support This is the most basic kind of docker querying you will find. Does not support HTTPS. Seems to work for local docker engines. Has not been tested against remote docker engines, or full URLs. Note that if you want this to work you'll have to configure docker to allow manipulation without being root, i.e., the user you connect as will need to be in the `docker` group. This was done instead of pulling in the `bollard` crate. Maybe I'm being silly, but `bollard` uses a whole lot of other crates in the name of being general and robust. These crates, however, add an unacceptable size to the final binary. (In the experiment I ran, on a release build, the binary size went from 2904696 to 4840968 bytes: an increase of 1.8 MB. With this patch the release binary is 2986360 bytes, which is an increase of 80k.) I wanted to see exactly what I could get away with when it came to talking to docker. This here actually seems like a fine compromise: HTTP is very simple if you only have to worry about one specific server, and JSON is not very hard to parse if you don't care too much about error handling, or are willing to play fast and loose with punctuation (which I am). --- Cargo.lock | 29 +- Cargo.toml | 1 + src/server/refresh.rs | 16 + src/server/refresh/docker.rs | 796 +++++++++++++++++++++++++++++++++++ 4 files changed, 839 insertions(+), 3 deletions(-) create mode 100644 src/server/refresh/docker.rs diff --git a/Cargo.lock b/Cargo.lock index 44eb670..6bba2e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dlib" version = "0.5.2" @@ -306,6 +312,7 @@ dependencies = [ "indoc", "log", "open", + "pretty_assertions", "procfs", "rand 0.8.5", "tempdir", @@ -446,7 +453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -657,6 +664,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -1012,9 +1029,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -1538,3 +1555,9 @@ name = "xkeysym" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index 1193872..6708067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ xdg = "2" [dev-dependencies] assert_matches = "1" +pretty_assertions = "1" tempdir = "0.3" [target.'cfg(target_os="linux")'.dependencies] diff --git a/src/server/refresh.rs b/src/server/refresh.rs index d269be0..c40c00f 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -7,12 +7,28 @@ use crate::message::PortDesc; #[cfg(target_os = "linux")] mod procfs; +#[cfg(unix)] +mod docker; + pub async fn get_entries() -> Result> { #[allow(unused)] let mut attempts = 0; let mut result: HashMap = HashMap::new(); + #[cfg(unix)] + { + attempts += 1; + match docker::get_entries().await { + Ok(m) => { + for (p, d) in m { + result.entry(p).or_insert(d); + } + } + Err(e) => error!("Error reading from docker: {e:?}"), + } + } + #[cfg(target_os = "linux")] { attempts += 1; diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs new file mode 100644 index 0000000..188d3c3 --- /dev/null +++ b/src/server/refresh/docker.rs @@ -0,0 +1,796 @@ +use anyhow::{bail, Context, Result}; +use std::collections::HashMap; +use tokio::io::{ + AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, +}; + +use crate::message::PortDesc; + +pub const DEFAULT_DOCKER_HOST: &str = "unix:///var/run/docker.sock"; + +async fn list_containers_with_connection(stream: T) -> Result> +where + T: AsyncRead + AsyncWrite + Unpin, +{ + // Send this one exact request. (Who needs an HTTP library?) + const DOCKER_LIST_CONTAINERS: &[u8] = b"\ +GET /containers/json HTTP/1.1\r\n\ +Host: localhost\r\n\ +User-Agent: fwd/1.0\r\n\ +Accept: */*\r\n\ +\r\n"; + let mut stream = tokio::io::BufStream::new(stream); + stream.write_all(DOCKER_LIST_CONTAINERS).await?; + stream.flush().await?; + + // Check the HTTP response. + let mut line = String::new(); + stream.read_line(&mut line).await?; + let parts: Vec<&str> = line.split(" ").collect(); + if parts.len() < 2 || parts[1] != "200" { + bail!("Error response from docker: {line}"); + } + + // Process the headers; all we really care about is content-length. + let mut content_length: usize = 0; + loop { + line.clear(); + stream.read_line(&mut line).await?; + if line.trim().is_empty() { + break; + } + line.make_ascii_lowercase(); + if let Some(rest) = line.strip_prefix("content-length: ") { + content_length = rest.trim().parse()?; + } + } + + // Read the JSON response. + let mut response_buffer = vec![0; content_length]; + stream.read_exact(&mut response_buffer).await?; + + // Done with the stream. + Ok(response_buffer) +} + +async fn list_containers() -> Result> { + let host = std::env::var("DOCKER_HOST") + .unwrap_or_else(|_| DEFAULT_DOCKER_HOST.to_string()); + match host { + h if h.starts_with("unix://") => { + let socket_path = &h[7..]; + let socket = tokio::net::UnixStream::connect(socket_path).await?; + list_containers_with_connection(socket).await + } + h if h.starts_with("tcp://") => { + let host_port = &h[6..]; // TODO: Routing to sub-paths? + let socket = tokio::net::TcpStream::connect(host_port).await?; + list_containers_with_connection(socket).await + } + h if h.starts_with("http://") => { + let host_port = &h[7..]; // TODO: Routing to sub-paths? + let socket = tokio::net::TcpStream::connect(host_port).await?; + list_containers_with_connection(socket).await + } + _ => bail!("Unsupported docker host: {host}"), + } +} + +#[derive(Debug, PartialEq)] +enum JsonValue { + Null, + True, + False, + Number(f64), + String(String), + Object(HashMap), + Array(Vec), +} + +impl JsonValue { + pub fn parse(blob: &[u8]) -> Result { + Self::parse_impl(blob).with_context(|| { + match std::str::from_utf8(blob) { + Ok(s) => format!("Failed to parse: {s}"), + Err(_) => format!("Failed to parse {blob:?}"), + } + }) + } + + fn parse_impl(blob: &[u8]) -> Result { + enum Tok { + Val(JsonValue), + StartObject, + StartArray, + } + + let mut stack = Vec::new(); + let mut i = 0; + while i < blob.len() { + match blob[i] { + b'n' => { + i += 4; + stack.push(Tok::Val(JsonValue::Null)); + } + b't' => { + i += 4; + stack.push(Tok::Val(JsonValue::True)); + } + b'f' => { + i += 5; + stack.push(Tok::Val(JsonValue::False)); + } + b'{' => { + i += 1; + stack.push(Tok::StartObject); + } + b'}' => { + i += 1; + let mut values = HashMap::new(); + loop { + match stack.pop() { + None => bail!("unexpected object terminator"), + Some(Tok::StartObject) => break, + Some(Tok::StartArray) => { + bail!("unterminated array") + } + Some(Tok::Val(v)) => match stack.pop() { + None => bail!( + "unexpected object terminator (mismatch)" + ), + Some(Tok::StartObject) => { + bail!("mismatch item count") + } + Some(Tok::StartArray) => { + bail!("unterminated array") + } + Some(Tok::Val(JsonValue::String(k))) => { + values.insert(k, v); + } + Some(Tok::Val(_)) => { + bail!("object keys must be strings") + } + }, + } + } + stack.push(Tok::Val(JsonValue::Object(values))); + } + b'[' => { + i += 1; + stack.push(Tok::StartArray); + } + b']' => { + i += 1; + let mut values = Vec::new(); + loop { + match stack.pop() { + None => bail!("unexpected array terminator"), + Some(Tok::StartObject) => { + bail!("unterminated object") + } + Some(Tok::StartArray) => break, + Some(Tok::Val(v)) => values.push(v), + } + } + values.reverse(); + stack.push(Tok::Val(JsonValue::Array(values))); + } + b'"' => { + i += 1; + let start = i; + while i < blob.len() { + if blob[i] == b'"' { + break; + } + if blob[i] == b'\\' { + i += 1; + } + i += 1; + } + if i == blob.len() { + bail!("Unterminated string at {i}"); + } + assert_eq!(blob[i], b'"'); + let mut chars = + std::str::from_utf8(&blob[start..i])?.chars(); + i += 1; // Consume the final quote. + + let mut value = String::new(); + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next().expect("mismatched escape") { + '"' => value.push('"'), + '\\' => value.push('\\'), + 'b' => value.push('\x08'), + 'f' => value.push('\x0C'), + 'n' => value.push('\n'), + 'r' => value.push('\r'), + 't' => value.push('\t'), + 'u' => { + // 4 hex + let mut temp = String::with_capacity(4); + for _ in 0..4 { + let Some(c) = chars.next() else { + bail!("not enough chars in unicode escape") + }; + temp.push(c); + } + let code = u32::from_str_radix(&temp, 16)?; + let Some(c) = char::from_u32(code) else { + bail!("invalid escape code {temp}") + }; + value.push(c); + } + _ => bail!("Invalid json escape"), + } + } else { + value.push(c); + } + } + + stack.push(Tok::Val(JsonValue::String(value))); + } + b',' => i += 1, // Value separator in object or array + b':' => i += 1, // Key/Value separator + x if x.is_ascii_whitespace() => i += 1, + x if x == b'-' || x.is_ascii_digit() => { + let start = i; + while i < blob.len() { + match blob[i] { + b' ' | b'\t' | b'\r' | b'\n' | b'{' | b'}' + | b'[' | b']' | b',' | b':' => { + break; + } + _ => i += 1, + } + } + let number: f64 = + std::str::from_utf8(&blob[start..i])?.parse()?; + stack.push(Tok::Val(JsonValue::Number(number))); + } + + x => bail!("Invalid json value start byte {x}"), + } + } + + match stack.pop().expect("underflow somehow") { + Tok::Val(v) => Ok(v), + Tok::StartObject => bail!("unterminated object"), + Tok::StartArray => bail!("unterminated array"), + } + } + + pub fn as_array(&self) -> Option<&[JsonValue]> { + match self { + JsonValue::Array(v) => Some(v), + _ => None, + } + } + + pub fn as_object(&self) -> Option<&HashMap> { + match self { + JsonValue::Object(v) => Some(v), + _ => None, + } + } + + pub fn as_string(&self) -> Option<&str> { + match self { + JsonValue::String(v) => Some(v), + _ => None, + } + } + + pub fn as_number(&self) -> Option { + match self { + JsonValue::Number(f) => Some(*f), + _ => None, + } + } +} + +pub async fn get_entries() -> Result> { + let mut h: HashMap = HashMap::new(); + + let response = list_containers().await?; + let response = JsonValue::parse(&response)?; + let Some(containers) = response.as_array() else { + bail!("Expected an array of containers") + }; + for container in containers { + let Some(container) = container.as_object() else { + bail!("Expected containers to be objects"); + }; + + let name = container + .get("Names") + .and_then(|n| n.as_array()) + .and_then(|n| n.first()) + .and_then(|n| n.as_string()) + .unwrap_or(""); + + for port in container + .get("Ports") + .and_then(|n| n.as_array()) + .unwrap_or(&[]) + { + let Some(port) = port.as_object() else { + bail!("port records must be objects") + }; + if let Some(public_port) = + port.get("PublicPort").and_then(|pp| pp.as_number()) + { + // NOTE: If these are really ports then `as u16` will be + // right, otherwise what are we even doing here? + let public_port = public_port.trunc() as u16; + let private_port = port + .get("PrivatePort") + .and_then(|pp| pp.as_number()) + .unwrap_or(0.0) + .trunc() as u16; + + h.insert( + public_port, + PortDesc { + port: public_port, + desc: format!("{name} (docker->{private_port})"), + }, + ); + } + } + } + + Ok(h) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn test_json_decode_basic() { + let cases: Vec<(&str, JsonValue)> = vec![ + ("12", JsonValue::Number(12.0)), + ("12.7", JsonValue::Number(12.7)), + ("-12", JsonValue::Number(-12.0)), + ("true", JsonValue::True), + ("false", JsonValue::False), + ("null", JsonValue::Null), + ("\"abcd\"", JsonValue::String("abcd".to_string())), + ( + "\" a \\\" \\r b \\n c \\f d \\b e \\t f \\\\ \"", + JsonValue::String( + " a \" \r b \n c \x0C d \x08 e \t f \\ ".to_string(), + ), + ), + ]; + for (blob, expected) in cases { + assert_eq!(JsonValue::parse(blob.as_bytes()).unwrap(), expected); + } + } + + #[test] + pub fn test_json_decode_array() { + let result = JsonValue::parse(b"[1, true, \"foo\", null]").unwrap(); + let JsonValue::Array(result) = result else { + panic!("Expected an array"); + }; + assert_eq!(result.len(), 4); + assert_eq!(result[0], JsonValue::Number(1.0)); + assert_eq!(result[1], JsonValue::True); + assert_eq!(result[2], JsonValue::String("foo".to_owned())); + assert_eq!(result[3], JsonValue::Null); + } + + #[test] + pub fn test_json_decode_array_empty() { + let result = JsonValue::parse(b"[]").unwrap(); + assert_eq!(result, JsonValue::Array(vec![])); + } + + #[test] + pub fn test_json_decode_array_nested() { + let result = JsonValue::parse(b"[1, [2, 3], 4]").unwrap(); + assert_eq!( + result, + JsonValue::Array(vec![ + JsonValue::Number(1.0), + JsonValue::Array(vec![ + JsonValue::Number(2.0), + JsonValue::Number(3.0), + ]), + JsonValue::Number(4.0) + ]) + ); + } + + #[test] + pub fn test_json_decode_object() { + let result = JsonValue::parse( + b"{\"a\": 1.0, \"b\": [2.0, 3.0], \"c\": {\"d\": 4.0}}", + ) + .unwrap(); + assert_eq!( + result, + JsonValue::Object(HashMap::from([ + ("a".to_owned(), JsonValue::Number(1.0)), + ( + "b".to_owned(), + JsonValue::Array(vec![ + JsonValue::Number(2.0), + JsonValue::Number(3.0), + ]) + ), + ( + "c".to_owned(), + JsonValue::Object(HashMap::from([( + "d".to_owned(), + JsonValue::Number(4.0) + )])) + ) + ])) + ) + } + + #[test] + pub fn test_json_decode_docker() { + use pretty_assertions::assert_eq; + + // This is the example container response from docker + let result = JsonValue::parse(b" +[ + { + \"Id\": \"8dfafdbc3a40\", + \"Names\": [ + \"/boring_feynman\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 1\", + \"Created\": 1367854155, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [ + { + \"PrivatePort\": 2222, + \"PublicPort\": 3333, + \"Type\": \"tcp\" + } + ], + \"Labels\": { + \"com.example.vendor\": \"Acme\", + \"com.example.license\": \"GPL\", + \"com.example.version\": \"1.0\" + }, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.docker.type\": \"container\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"2cdc4edb1ded3631c81f57966563e5c8525b81121bb3706a9a9a3ae102711f3f\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.2\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:02\" + } + } + }, + \"Mounts\": [ + { + \"Name\": \"fac362...80535\", + \"Source\": \"/data\", + \"Destination\": \"/data\", + \"Driver\": \"local\", + \"Mode\": \"ro,Z\", + \"RW\": false, + \"Propagation\": \"\" + } + ] + }, + { + \"Id\": \"9cd87474be90\", + \"Names\": [ + \"/coolName\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 222222\", + \"Created\": 1367854155, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [], + \"Labels\": {}, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.docker.type\": \"container\", + \"io.kubernetes.sandbox.id\": \"3befe639bed0fd6afdd65fd1fa84506756f59360ec4adc270b0fdac9be22b4d3\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"88eaed7b37b38c2a3f0c4bc796494fdf51b270c2d22656412a2ca5d559a64d7a\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.8\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:08\" + } + } + }, + \"Mounts\": [] + }, + { + \"Id\": \"3176a2479c92\", + \"Names\": [ + \"/sleepy_dog\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 3333333333333333\", + \"Created\": 1367854154, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [], + \"Labels\": {}, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.image.id\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"io.kubernetes.image.name\": \"ubuntu:latest\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"8b27c041c30326d59cd6e6f510d4f8d1d570a228466f956edf7815508f78e30d\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.6\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:06\" + } + } + }, + \"Mounts\": [] + }, + { + \"Id\": \"4cb07b47f9fb\", + \"Names\": [ + \"/running_cat\" + ], + \"Image\": \"ubuntu:latest\", + \"ImageID\": \"d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82\", + \"Command\": \"echo 444444444444444444444444444444444\", + \"Created\": 1367854152, + \"State\": \"Exited\", + \"Status\": \"Exit 0\", + \"Ports\": [], + \"Labels\": {}, + \"SizeRw\": 12288, + \"SizeRootFs\": 0, + \"HostConfig\": { + \"NetworkMode\": \"default\", + \"Annotations\": { + \"io.kubernetes.config.source\": \"api\" + } + }, + \"NetworkSettings\": { + \"Networks\": { + \"bridge\": { + \"NetworkID\": \"7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812\", + \"EndpointID\": \"d91c7b2f0644403d7ef3095985ea0e2370325cd2332ff3a3225c4247328e66e9\", + \"Gateway\": \"172.17.0.1\", + \"IPAddress\": \"172.17.0.5\", + \"IPPrefixLen\": 16, + \"IPv6Gateway\": \"\", + \"GlobalIPv6Address\": \"\", + \"GlobalIPv6PrefixLen\": 0, + \"MacAddress\": \"02:42:ac:11:00:05\" + } + } + }, + \"Mounts\": [] + } +] +").unwrap(); + let expected = JsonValue::Array(vec![ + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("8dfafdbc3a40".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/boring_feynman".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 1".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854155_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![ + JsonValue::Object(HashMap::from([ + ("PrivatePort".to_owned(), JsonValue::Number(2222_f64)), + ("PublicPort".to_owned(), JsonValue::Number(3333_f64)), + ("Type".to_owned(), JsonValue::String("tcp".to_owned())) + ])) + ])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([ + ("com.example.vendor".to_owned(), JsonValue::String("Acme".to_owned())), + ("com.example.license".to_owned(), JsonValue::String("GPL".to_owned())), + ("com.example.version".to_owned(), JsonValue::String("1.0".to_owned())) + ]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.docker.type".to_owned(), JsonValue::String("container".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("2cdc4edb1ded3631c81f57966563e5c8525b81121bb3706a9a9a3ae102711f3f".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.2".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:02".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![ + JsonValue::Object(HashMap::from([ + ("Name".to_owned(), JsonValue::String("fac362...80535".to_owned())), + ("Source".to_owned(), JsonValue::String("/data".to_owned())), + ("Destination".to_owned(), JsonValue::String("/data".to_owned())), + ("Driver".to_owned(), JsonValue::String("local".to_owned())), + ("Mode".to_owned(), JsonValue::String("ro,Z".to_owned())), + ("RW".to_owned(), JsonValue::False), + ("Propagation".to_owned(), JsonValue::String("".to_owned())) + ])) + ])) + ])), + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("9cd87474be90".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/coolName".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 222222".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854155_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.docker.type".to_owned(), JsonValue::String("container".to_owned())), + ("io.kubernetes.sandbox.id".to_owned(), JsonValue::String("3befe639bed0fd6afdd65fd1fa84506756f59360ec4adc270b0fdac9be22b4d3".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("88eaed7b37b38c2a3f0c4bc796494fdf51b270c2d22656412a2ca5d559a64d7a".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.8".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:08".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![])) + ])), + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("3176a2479c92".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/sleepy_dog".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 3333333333333333".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854154_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.image.id".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("io.kubernetes.image.name".to_owned(), JsonValue::String("ubuntu:latest".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("8b27c041c30326d59cd6e6f510d4f8d1d570a228466f956edf7815508f78e30d".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.6".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:06".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![])) + ])), + JsonValue::Object(HashMap::from([ + ("Id".to_owned(), JsonValue::String("4cb07b47f9fb".to_owned())), + ("Names".to_owned(), JsonValue::Array(vec![ + JsonValue::String("/running_cat".to_owned()) + ])), + ("Image".to_owned(), JsonValue::String("ubuntu:latest".to_owned())), + ("ImageID".to_owned(), JsonValue::String("d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82".to_owned())), + ("Command".to_owned(), JsonValue::String("echo 444444444444444444444444444444444".to_owned())), + ("Created".to_owned(), JsonValue::Number(1367854152_f64)), + ("State".to_owned(), JsonValue::String("Exited".to_owned())), + ("Status".to_owned(), JsonValue::String("Exit 0".to_owned())), + ("Ports".to_owned(), JsonValue::Array(vec![])), + ("Labels".to_owned(), JsonValue::Object(HashMap::from([]))), + ("SizeRw".to_owned(), JsonValue::Number(12288_f64)), + ("SizeRootFs".to_owned(), JsonValue::Number(0_f64)), + ("HostConfig".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkMode".to_owned(), JsonValue::String("default".to_owned())), + ("Annotations".to_owned(), JsonValue::Object(HashMap::from([ + ("io.kubernetes.config.source".to_owned(), JsonValue::String("api".to_owned())) + ]))) + ]))), + ("NetworkSettings".to_owned(), JsonValue::Object(HashMap::from([ + ("Networks".to_owned(), JsonValue::Object(HashMap::from([ + ("bridge".to_owned(), JsonValue::Object(HashMap::from([ + ("NetworkID".to_owned(), JsonValue::String("7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812".to_owned())), + ("EndpointID".to_owned(), JsonValue::String("d91c7b2f0644403d7ef3095985ea0e2370325cd2332ff3a3225c4247328e66e9".to_owned())), + ("Gateway".to_owned(), JsonValue::String("172.17.0.1".to_owned())), + ("IPAddress".to_owned(), JsonValue::String("172.17.0.5".to_owned())), + ("IPPrefixLen".to_owned(), JsonValue::Number(16_f64)), + ("IPv6Gateway".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6Address".to_owned(), JsonValue::String("".to_owned())), + ("GlobalIPv6PrefixLen".to_owned(), JsonValue::Number(0_f64)), + ("MacAddress".to_owned(), JsonValue::String("02:42:ac:11:00:05".to_owned())) + ]))) + ]))) + ]))), + ("Mounts".to_owned(), JsonValue::Array(vec![])) + ])) + ]); + assert_eq!(result, expected); + } +} From b983595049f07f1932c70f0a19626738ecc201bb Mon Sep 17 00:00:00 2001 From: Brandon W Maister Date: Wed, 31 Jul 2024 15:10:11 +0000 Subject: [PATCH 29/85] feat: Show errored ports as an error state --- src/client/ui.rs | 71 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index c6cfa9f..a20a8e1 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -13,13 +13,14 @@ use log::{error, info, Level, Metadata, Record}; use std::collections::vec_deque::VecDeque; use std::collections::{HashMap, HashSet}; use std::io::stdout; +use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio_stream::StreamExt; use tui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout, Margin, Rect}, - style::{Color, Style}, + style::{Color, Modifier, Style}, widgets::{ Block, Borders, List, ListItem, ListState, Row, Table, TableState, }, @@ -67,9 +68,22 @@ impl log::Log for Logger { fn flush(&self) {} } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum State { + Enabled, + Broken, + Disabled, +} + +impl State { + fn boxed(self) -> Arc> { + Arc::new(Mutex::new(self)) + } +} + #[derive(Debug)] struct Listener { - enabled: bool, + state: std::sync::Arc>, stop: Option>, desc: Option, } @@ -80,7 +94,15 @@ impl Listener { desc: PortDesc, enabled: bool, ) -> Listener { - let mut listener = Listener { enabled, stop: None, desc: Some(desc) }; + let mut listener = Listener { + state: if enabled { + State::Enabled.boxed() + } else { + State::Disabled.boxed() + }, + stop: None, + desc: Some(desc), + }; if enabled { listener.start(socks_port); } @@ -88,15 +110,19 @@ impl Listener { } pub fn enabled(&self) -> bool { - self.enabled + self.state() == State::Enabled + } + + fn state(&self) -> State { + *self.state.lock().unwrap() } pub fn set_enabled(&mut self, socks_port: Option, enabled: bool) { if enabled { - self.enabled = true; + self.state = State::Enabled.boxed(); self.start(socks_port); } else { - self.enabled = false; + self.state = State::Disabled.boxed(); self.stop = None; } } @@ -112,19 +138,22 @@ impl Listener { } pub fn start(&mut self, socks_port: Option) { - if self.enabled { + if self.enabled() { if let (Some(desc), Some(socks_port), None) = (&self.desc, socks_port, &self.stop) { info!("Starting port {port} to {socks_port}", port = desc.port); let (l, stop) = oneshot::channel(); let port = desc.port; + let state = self.state.clone(); tokio::spawn(async move { let result = tokio::select! { r = client_listen(port, socks_port) => r, _ = stop => Ok(()), }; if let Err(e) = result { + let mut sg = state.lock().unwrap(); + *sg = State::Broken; error!("Error listening on port {port}: {e:?}"); } else { info!("Stopped listening on port {port}"); @@ -243,6 +272,8 @@ impl UI { fn render_ports(&mut self, frame: &mut Frame, size: Rect) { let enabled_port_style = Style::default(); let disabled_port_style = Style::default().fg(Color::DarkGray); + let broken_port_style = + Style::default().fg(Color::Red).add_modifier(Modifier::DIM); let mut rows = Vec::new(); let ports = self.get_ui_ports(); @@ -250,20 +281,18 @@ impl UI { ports.iter().map(|p| format!("{p}")).collect(); for (index, port) in ports.into_iter().enumerate() { let listener = self.ports.get(&port).unwrap(); + let (symbol, style) = match listener.state() { + State::Enabled => (" ✓ ", enabled_port_style), + State::Broken => (" ✗ ", broken_port_style), + State::Disabled => ("", disabled_port_style), + }; rows.push( Row::new(vec![ - if listener.enabled { " ✓ " } else { "" }, - &port_strings[index][..], - match &listener.desc { - Some(port_desc) => &port_desc.desc, - None => "", - }, + symbol, + &*port_strings[index], + listener.desc.as_ref().map(|pd| &*pd.desc).unwrap_or(""), ]) - .style(if listener.enabled { - enabled_port_style - } else { - disabled_port_style - }), + .style(style), ); } @@ -357,7 +386,11 @@ impl UI { } fn enable_disable_port(&mut self, port: u16) { - if let Some(listener) = self.ports.get_mut(&port) { + let state = self.ports.get(&port).map(Listener::state); + if state == Some(State::Broken) { + // try turning it off and on again, it will at least get logs visible + self.ports.remove(&port); + } else if let Some(listener) = self.ports.get_mut(&port) { listener.set_enabled(self.socks_port, !listener.enabled()); } } From 9ef5515f01fcc5164c4257e008c9b4c1c8e60ca3 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 6 Aug 2024 06:51:03 -0700 Subject: [PATCH 30/85] Clippy --- src/client/mod.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 79b4b15..18bd3fc 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -62,7 +62,7 @@ async fn client_sync( } => result, }; - if let Err(_) = result { + if result.is_err() { // Something went wrong, let's just make sure we flush the client's // stderr before we return. _ = stderr.write_all(&buf[..]).await; @@ -121,7 +121,7 @@ async fn client_handle_connection( 0, // ..ho.. 1, // ..st ((port & 0xFF00) >> 8).try_into().unwrap(), // port (high) - ((port & 0x00FF) >> 0).try_into().unwrap(), // port (low) + (port & 0x00FF).try_into().unwrap(), // port (low) ]; dest_socket.write_all(&packet[..]).await?; @@ -306,14 +306,14 @@ async fn client_main( } } => { if let Err(e) = result { - print!("Error sending refreshes\n"); + println!("Error sending refreshes"); return Err(e.into()); } }, result = client_handle_messages(reader, events) => { if let Err(e) = result { - print!("Error handling messages\n"); - return Err(e.into()); + println!("Error handling messages"); + return Err(e); } }, } @@ -399,18 +399,14 @@ async fn client_connect_loop( if let Err(e) = client_sync(&mut reader, &mut stderr).await { error!("Error synchronizing: {:?}", e); - match child.wait().await { - Ok(status) => { - if is_sigint(status) { - return; - } else { - match status.code() { - Some(127) => eprintln!("Cannot find `fwd` remotely, make sure it is installed"), - _ => (), - }; - } + if let Ok(status) = child.wait().await { + if is_sigint(status) { + return; + } else if let Some(127) = status.code() { + eprintln!( + "Cannot find `fwd` remotely, make sure it is installed" + ); } - Err(_) => (), }; tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; From 8a60f891109334381cf745ec84db4e85dd252597 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 6 Aug 2024 09:22:11 -0700 Subject: [PATCH 31/85] Fix small JSON bugs, bring in test suite Just decided to "harden" the JSON parser a little bit with the test suite from https://github.com/nst/JSONTestSuite. Now I'm pretty sure that we can handle whatever JSON docker throws at us. --- resources/json/README.md | 14 +++ .../json/i_number_double_huge_neg_exp.json | 1 + resources/json/i_number_huge_exp.json | 1 + resources/json/i_number_neg_int_huge_exp.json | 1 + .../json/i_number_pos_double_huge_exp.json | 1 + .../json/i_number_real_neg_overflow.json | 1 + .../json/i_number_real_pos_overflow.json | 1 + resources/json/i_number_real_underflow.json | 1 + resources/json/i_number_too_big_neg_int.json | 1 + resources/json/i_number_too_big_pos_int.json | 1 + .../json/i_number_very_big_negative_int.json | 1 + .../json/i_object_key_lone_2nd_surrogate.json | 1 + ..._string_1st_surrogate_but_2nd_missing.json | 1 + ...tring_1st_valid_surrogate_2nd_invalid.json | 1 + .../json/i_string_UTF-8_invalid_sequence.json | 1 + .../json/i_string_UTF8_surrogate_U+D800.json | 1 + ...incomplete_surrogate_and_escape_valid.json | 1 + .../i_string_incomplete_surrogate_pair.json | 1 + ...ng_incomplete_surrogates_escape_valid.json | 1 + .../i_string_invalid_lonely_surrogate.json | 1 + .../json/i_string_invalid_surrogate.json | 1 + resources/json/i_string_invalid_utf-8.json | 1 + .../i_string_inverted_surrogates_U+1D11E.json | 1 + resources/json/i_string_iso_latin_1.json | 1 + .../json/i_string_lone_second_surrogate.json | 1 + .../i_string_lone_utf8_continuation_byte.json | 1 + .../json/i_string_not_in_unicode_range.json | 1 + .../i_string_overlong_sequence_2_bytes.json | 1 + .../i_string_overlong_sequence_6_bytes.json | 1 + ...string_overlong_sequence_6_bytes_null.json | 1 + resources/json/i_string_truncated-utf-8.json | 1 + .../json/i_structure_500_nested_arrays.json | 1 + resources/json/y_array_arraysWithSpaces.json | 1 + resources/json/y_array_empty-string.json | 1 + resources/json/y_array_empty.json | 1 + .../json/y_array_ending_with_newline.json | 1 + resources/json/y_array_false.json | 1 + resources/json/y_array_heterogeneous.json | 1 + resources/json/y_array_null.json | 1 + .../json/y_array_with_1_and_newline.json | 2 + .../json/y_array_with_leading_space.json | 1 + resources/json/y_array_with_several_null.json | 1 + .../json/y_array_with_trailing_space.json | 1 + resources/json/y_number.json | 1 + resources/json/y_number_0e+1.json | 1 + resources/json/y_number_0e1.json | 1 + resources/json/y_number_after_space.json | 1 + .../json/y_number_double_close_to_zero.json | 1 + resources/json/y_number_int_with_exp.json | 1 + resources/json/y_number_minus_zero.json | 1 + resources/json/y_number_negative_int.json | 1 + resources/json/y_number_negative_one.json | 1 + resources/json/y_number_negative_zero.json | 1 + resources/json/y_number_real_capital_e.json | 1 + .../json/y_number_real_capital_e_neg_exp.json | 1 + .../json/y_number_real_capital_e_pos_exp.json | 1 + resources/json/y_number_real_exponent.json | 1 + .../json/y_number_real_fraction_exponent.json | 1 + resources/json/y_number_real_neg_exp.json | 1 + .../json/y_number_real_pos_exponent.json | 1 + resources/json/y_number_simple_int.json | 1 + resources/json/y_number_simple_real.json | 1 + resources/json/y_object.json | 1 + resources/json/y_object_basic.json | 1 + resources/json/y_object_duplicated_key.json | 1 + .../y_object_duplicated_key_and_value.json | 1 + resources/json/y_object_empty.json | 1 + resources/json/y_object_empty_key.json | 1 + .../json/y_object_escaped_null_in_key.json | 1 + resources/json/y_object_extreme_numbers.json | 1 + resources/json/y_object_long_strings.json | 1 + resources/json/y_object_simple.json | 1 + resources/json/y_object_string_unicode.json | 1 + resources/json/y_object_with_newlines.json | 3 + .../y_string_1_2_3_bytes_UTF-8_sequences.json | 1 + .../y_string_accepted_surrogate_pair.json | 1 + .../y_string_accepted_surrogate_pairs.json | 1 + resources/json/y_string_allowed_escapes.json | 1 + ...y_string_backslash_and_u_escaped_zero.json | 1 + .../json/y_string_backslash_doublequotes.json | 1 + resources/json/y_string_comments.json | 1 + resources/json/y_string_double_escape_a.json | 1 + resources/json/y_string_double_escape_n.json | 1 + .../y_string_escaped_control_character.json | 1 + .../json/y_string_escaped_noncharacter.json | 1 + resources/json/y_string_in_array.json | 1 + .../y_string_in_array_with_leading_space.json | 1 + .../y_string_last_surrogates_1_and_2.json | 1 + resources/json/y_string_nbsp_uescaped.json | 1 + ...y_string_nonCharacterInUTF-8_U+10FFFF.json | 1 + .../y_string_nonCharacterInUTF-8_U+FFFF.json | 1 + resources/json/y_string_null_escape.json | 1 + resources/json/y_string_one-byte-utf-8.json | 1 + resources/json/y_string_pi.json | 1 + ...ring_reservedCharacterInUTF-8_U+1BFFF.json | 1 + resources/json/y_string_simple_ascii.json | 1 + resources/json/y_string_space.json | 1 + ...rogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json | 1 + resources/json/y_string_three-byte-utf-8.json | 1 + resources/json/y_string_two-byte-utf-8.json | 1 + resources/json/y_string_u+2028_line_sep.json | 1 + resources/json/y_string_u+2029_par_sep.json | 1 + resources/json/y_string_uEscape.json | 1 + resources/json/y_string_uescaped_newline.json | 1 + .../json/y_string_unescaped_char_delete.json | 1 + resources/json/y_string_unicode.json | 1 + .../y_string_unicodeEscapedBackslash.json | 1 + resources/json/y_string_unicode_2.json | 1 + .../y_string_unicode_U+10FFFE_nonchar.json | 1 + .../y_string_unicode_U+1FFFE_nonchar.json | 1 + ...tring_unicode_U+200B_ZERO_WIDTH_SPACE.json | 1 + ..._string_unicode_U+2064_invisible_plus.json | 1 + .../json/y_string_unicode_U+FDD0_nonchar.json | 1 + .../json/y_string_unicode_U+FFFE_nonchar.json | 1 + ...y_string_unicode_escaped_double_quote.json | 1 + resources/json/y_string_utf8.json | 1 + .../json/y_string_with_del_character.json | 1 + resources/json/y_structure_lonely_false.json | 1 + resources/json/y_structure_lonely_int.json | 1 + .../y_structure_lonely_negative_real.json | 1 + resources/json/y_structure_lonely_null.json | 1 + resources/json/y_structure_lonely_string.json | 1 + resources/json/y_structure_lonely_true.json | 1 + resources/json/y_structure_string_empty.json | 1 + .../json/y_structure_trailing_newline.json | 1 + resources/json/y_structure_true_in_array.json | 1 + .../json/y_structure_whitespace_array.json | 1 + src/server/refresh/docker.rs | 107 ++++++++++++++---- 128 files changed, 231 insertions(+), 19 deletions(-) create mode 100644 resources/json/README.md create mode 100644 resources/json/i_number_double_huge_neg_exp.json create mode 100644 resources/json/i_number_huge_exp.json create mode 100755 resources/json/i_number_neg_int_huge_exp.json create mode 100755 resources/json/i_number_pos_double_huge_exp.json create mode 100644 resources/json/i_number_real_neg_overflow.json create mode 100644 resources/json/i_number_real_pos_overflow.json create mode 100644 resources/json/i_number_real_underflow.json create mode 100644 resources/json/i_number_too_big_neg_int.json create mode 100644 resources/json/i_number_too_big_pos_int.json create mode 100755 resources/json/i_number_very_big_negative_int.json create mode 100644 resources/json/i_object_key_lone_2nd_surrogate.json create mode 100644 resources/json/i_string_1st_surrogate_but_2nd_missing.json create mode 100644 resources/json/i_string_1st_valid_surrogate_2nd_invalid.json create mode 100755 resources/json/i_string_UTF-8_invalid_sequence.json create mode 100644 resources/json/i_string_UTF8_surrogate_U+D800.json create mode 100755 resources/json/i_string_incomplete_surrogate_and_escape_valid.json create mode 100755 resources/json/i_string_incomplete_surrogate_pair.json create mode 100755 resources/json/i_string_incomplete_surrogates_escape_valid.json create mode 100755 resources/json/i_string_invalid_lonely_surrogate.json create mode 100755 resources/json/i_string_invalid_surrogate.json create mode 100644 resources/json/i_string_invalid_utf-8.json create mode 100755 resources/json/i_string_inverted_surrogates_U+1D11E.json create mode 100644 resources/json/i_string_iso_latin_1.json create mode 100644 resources/json/i_string_lone_second_surrogate.json create mode 100644 resources/json/i_string_lone_utf8_continuation_byte.json create mode 100644 resources/json/i_string_not_in_unicode_range.json create mode 100644 resources/json/i_string_overlong_sequence_2_bytes.json create mode 100755 resources/json/i_string_overlong_sequence_6_bytes.json create mode 100755 resources/json/i_string_overlong_sequence_6_bytes_null.json create mode 100644 resources/json/i_string_truncated-utf-8.json create mode 100644 resources/json/i_structure_500_nested_arrays.json create mode 100755 resources/json/y_array_arraysWithSpaces.json create mode 100644 resources/json/y_array_empty-string.json create mode 100755 resources/json/y_array_empty.json create mode 100755 resources/json/y_array_ending_with_newline.json create mode 100644 resources/json/y_array_false.json create mode 100755 resources/json/y_array_heterogeneous.json create mode 100644 resources/json/y_array_null.json create mode 100644 resources/json/y_array_with_1_and_newline.json create mode 100755 resources/json/y_array_with_leading_space.json create mode 100755 resources/json/y_array_with_several_null.json create mode 100755 resources/json/y_array_with_trailing_space.json create mode 100644 resources/json/y_number.json create mode 100755 resources/json/y_number_0e+1.json create mode 100755 resources/json/y_number_0e1.json create mode 100644 resources/json/y_number_after_space.json create mode 100755 resources/json/y_number_double_close_to_zero.json create mode 100755 resources/json/y_number_int_with_exp.json create mode 100755 resources/json/y_number_minus_zero.json create mode 100644 resources/json/y_number_negative_int.json create mode 100644 resources/json/y_number_negative_one.json create mode 100644 resources/json/y_number_negative_zero.json create mode 100644 resources/json/y_number_real_capital_e.json create mode 100644 resources/json/y_number_real_capital_e_neg_exp.json create mode 100644 resources/json/y_number_real_capital_e_pos_exp.json create mode 100644 resources/json/y_number_real_exponent.json create mode 100644 resources/json/y_number_real_fraction_exponent.json create mode 100644 resources/json/y_number_real_neg_exp.json create mode 100644 resources/json/y_number_real_pos_exponent.json create mode 100644 resources/json/y_number_simple_int.json create mode 100644 resources/json/y_number_simple_real.json create mode 100755 resources/json/y_object.json create mode 100755 resources/json/y_object_basic.json create mode 100755 resources/json/y_object_duplicated_key.json create mode 100755 resources/json/y_object_duplicated_key_and_value.json create mode 100644 resources/json/y_object_empty.json create mode 100755 resources/json/y_object_empty_key.json create mode 100644 resources/json/y_object_escaped_null_in_key.json create mode 100644 resources/json/y_object_extreme_numbers.json create mode 100644 resources/json/y_object_long_strings.json create mode 100644 resources/json/y_object_simple.json create mode 100644 resources/json/y_object_string_unicode.json create mode 100644 resources/json/y_object_with_newlines.json create mode 100755 resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json create mode 100755 resources/json/y_string_accepted_surrogate_pair.json create mode 100755 resources/json/y_string_accepted_surrogate_pairs.json create mode 100644 resources/json/y_string_allowed_escapes.json create mode 100755 resources/json/y_string_backslash_and_u_escaped_zero.json create mode 100644 resources/json/y_string_backslash_doublequotes.json create mode 100644 resources/json/y_string_comments.json create mode 100644 resources/json/y_string_double_escape_a.json create mode 100644 resources/json/y_string_double_escape_n.json create mode 100644 resources/json/y_string_escaped_control_character.json create mode 100755 resources/json/y_string_escaped_noncharacter.json create mode 100755 resources/json/y_string_in_array.json create mode 100755 resources/json/y_string_in_array_with_leading_space.json create mode 100644 resources/json/y_string_last_surrogates_1_and_2.json create mode 100644 resources/json/y_string_nbsp_uescaped.json create mode 100755 resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json create mode 100755 resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json create mode 100644 resources/json/y_string_null_escape.json create mode 100644 resources/json/y_string_one-byte-utf-8.json create mode 100644 resources/json/y_string_pi.json create mode 100755 resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json create mode 100644 resources/json/y_string_simple_ascii.json create mode 100644 resources/json/y_string_space.json create mode 100755 resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json create mode 100644 resources/json/y_string_three-byte-utf-8.json create mode 100644 resources/json/y_string_two-byte-utf-8.json create mode 100755 resources/json/y_string_u+2028_line_sep.json create mode 100755 resources/json/y_string_u+2029_par_sep.json create mode 100755 resources/json/y_string_uEscape.json create mode 100644 resources/json/y_string_uescaped_newline.json create mode 100755 resources/json/y_string_unescaped_char_delete.json create mode 100644 resources/json/y_string_unicode.json create mode 100755 resources/json/y_string_unicodeEscapedBackslash.json create mode 100644 resources/json/y_string_unicode_2.json create mode 100644 resources/json/y_string_unicode_U+10FFFE_nonchar.json create mode 100644 resources/json/y_string_unicode_U+1FFFE_nonchar.json create mode 100644 resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json create mode 100644 resources/json/y_string_unicode_U+2064_invisible_plus.json create mode 100644 resources/json/y_string_unicode_U+FDD0_nonchar.json create mode 100644 resources/json/y_string_unicode_U+FFFE_nonchar.json create mode 100755 resources/json/y_string_unicode_escaped_double_quote.json create mode 100644 resources/json/y_string_utf8.json create mode 100755 resources/json/y_string_with_del_character.json create mode 100644 resources/json/y_structure_lonely_false.json create mode 100755 resources/json/y_structure_lonely_int.json create mode 100755 resources/json/y_structure_lonely_negative_real.json create mode 100644 resources/json/y_structure_lonely_null.json create mode 100755 resources/json/y_structure_lonely_string.json create mode 100755 resources/json/y_structure_lonely_true.json create mode 100644 resources/json/y_structure_string_empty.json create mode 100644 resources/json/y_structure_trailing_newline.json create mode 100644 resources/json/y_structure_true_in_array.json create mode 100644 resources/json/y_structure_whitespace_array.json diff --git a/resources/json/README.md b/resources/json/README.md new file mode 100644 index 0000000..533092f --- /dev/null +++ b/resources/json/README.md @@ -0,0 +1,14 @@ +# Test JSON + +This directory contains test JSON files from https://github.com/nst/JSONTestSuite as of commit 984defc. + +It only has the positive and questionable JSON inputs, as our JSON parser is extremely forgiving, by design. + +## Filtered tests + +Some of the questionable tests have been removed: + +- `i_structure_UTF-8_BOM_empty_object.json` removed because we don't handle BOMs. +- `i_string_utf16LE_no_BOM.json` removed because we don't speak UTF16. +- `i_string_utf16BE_no_BOM.json` removed because we don't speak UTF16. +- `i_string_UTF-16LE_with_BOM.json` removed because we don't speak UTF16. diff --git a/resources/json/i_number_double_huge_neg_exp.json b/resources/json/i_number_double_huge_neg_exp.json new file mode 100644 index 0000000..ae4c7b7 --- /dev/null +++ b/resources/json/i_number_double_huge_neg_exp.json @@ -0,0 +1 @@ +[123.456e-789] \ No newline at end of file diff --git a/resources/json/i_number_huge_exp.json b/resources/json/i_number_huge_exp.json new file mode 100644 index 0000000..9b5efa2 --- /dev/null +++ b/resources/json/i_number_huge_exp.json @@ -0,0 +1 @@ +[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006] \ No newline at end of file diff --git a/resources/json/i_number_neg_int_huge_exp.json b/resources/json/i_number_neg_int_huge_exp.json new file mode 100755 index 0000000..3abd58a --- /dev/null +++ b/resources/json/i_number_neg_int_huge_exp.json @@ -0,0 +1 @@ +[-1e+9999] \ No newline at end of file diff --git a/resources/json/i_number_pos_double_huge_exp.json b/resources/json/i_number_pos_double_huge_exp.json new file mode 100755 index 0000000..e10a7eb --- /dev/null +++ b/resources/json/i_number_pos_double_huge_exp.json @@ -0,0 +1 @@ +[1.5e+9999] \ No newline at end of file diff --git a/resources/json/i_number_real_neg_overflow.json b/resources/json/i_number_real_neg_overflow.json new file mode 100644 index 0000000..3d628a9 --- /dev/null +++ b/resources/json/i_number_real_neg_overflow.json @@ -0,0 +1 @@ +[-123123e100000] \ No newline at end of file diff --git a/resources/json/i_number_real_pos_overflow.json b/resources/json/i_number_real_pos_overflow.json new file mode 100644 index 0000000..54d7d3d --- /dev/null +++ b/resources/json/i_number_real_pos_overflow.json @@ -0,0 +1 @@ +[123123e100000] \ No newline at end of file diff --git a/resources/json/i_number_real_underflow.json b/resources/json/i_number_real_underflow.json new file mode 100644 index 0000000..c5236eb --- /dev/null +++ b/resources/json/i_number_real_underflow.json @@ -0,0 +1 @@ +[123e-10000000] \ No newline at end of file diff --git a/resources/json/i_number_too_big_neg_int.json b/resources/json/i_number_too_big_neg_int.json new file mode 100644 index 0000000..dfa3846 --- /dev/null +++ b/resources/json/i_number_too_big_neg_int.json @@ -0,0 +1 @@ +[-123123123123123123123123123123] \ No newline at end of file diff --git a/resources/json/i_number_too_big_pos_int.json b/resources/json/i_number_too_big_pos_int.json new file mode 100644 index 0000000..338a8c3 --- /dev/null +++ b/resources/json/i_number_too_big_pos_int.json @@ -0,0 +1 @@ +[100000000000000000000] \ No newline at end of file diff --git a/resources/json/i_number_very_big_negative_int.json b/resources/json/i_number_very_big_negative_int.json new file mode 100755 index 0000000..e2d9738 --- /dev/null +++ b/resources/json/i_number_very_big_negative_int.json @@ -0,0 +1 @@ +[-237462374673276894279832749832423479823246327846] \ No newline at end of file diff --git a/resources/json/i_object_key_lone_2nd_surrogate.json b/resources/json/i_object_key_lone_2nd_surrogate.json new file mode 100644 index 0000000..5be7eba --- /dev/null +++ b/resources/json/i_object_key_lone_2nd_surrogate.json @@ -0,0 +1 @@ +{"\uDFAA":0} \ No newline at end of file diff --git a/resources/json/i_string_1st_surrogate_but_2nd_missing.json b/resources/json/i_string_1st_surrogate_but_2nd_missing.json new file mode 100644 index 0000000..3b9e37c --- /dev/null +++ b/resources/json/i_string_1st_surrogate_but_2nd_missing.json @@ -0,0 +1 @@ +["\uDADA"] \ No newline at end of file diff --git a/resources/json/i_string_1st_valid_surrogate_2nd_invalid.json b/resources/json/i_string_1st_valid_surrogate_2nd_invalid.json new file mode 100644 index 0000000..4875928 --- /dev/null +++ b/resources/json/i_string_1st_valid_surrogate_2nd_invalid.json @@ -0,0 +1 @@ +["\uD888\u1234"] \ No newline at end of file diff --git a/resources/json/i_string_UTF-8_invalid_sequence.json b/resources/json/i_string_UTF-8_invalid_sequence.json new file mode 100755 index 0000000..e2a968a --- /dev/null +++ b/resources/json/i_string_UTF-8_invalid_sequence.json @@ -0,0 +1 @@ +["日шú"] \ No newline at end of file diff --git a/resources/json/i_string_UTF8_surrogate_U+D800.json b/resources/json/i_string_UTF8_surrogate_U+D800.json new file mode 100644 index 0000000..916bff9 --- /dev/null +++ b/resources/json/i_string_UTF8_surrogate_U+D800.json @@ -0,0 +1 @@ +["í €"] \ No newline at end of file diff --git a/resources/json/i_string_incomplete_surrogate_and_escape_valid.json b/resources/json/i_string_incomplete_surrogate_and_escape_valid.json new file mode 100755 index 0000000..3cb11d2 --- /dev/null +++ b/resources/json/i_string_incomplete_surrogate_and_escape_valid.json @@ -0,0 +1 @@ +["\uD800\n"] \ No newline at end of file diff --git a/resources/json/i_string_incomplete_surrogate_pair.json b/resources/json/i_string_incomplete_surrogate_pair.json new file mode 100755 index 0000000..38ec23b --- /dev/null +++ b/resources/json/i_string_incomplete_surrogate_pair.json @@ -0,0 +1 @@ +["\uDd1ea"] \ No newline at end of file diff --git a/resources/json/i_string_incomplete_surrogates_escape_valid.json b/resources/json/i_string_incomplete_surrogates_escape_valid.json new file mode 100755 index 0000000..c9cd6f6 --- /dev/null +++ b/resources/json/i_string_incomplete_surrogates_escape_valid.json @@ -0,0 +1 @@ +["\uD800\uD800\n"] \ No newline at end of file diff --git a/resources/json/i_string_invalid_lonely_surrogate.json b/resources/json/i_string_invalid_lonely_surrogate.json new file mode 100755 index 0000000..3abbd8d --- /dev/null +++ b/resources/json/i_string_invalid_lonely_surrogate.json @@ -0,0 +1 @@ +["\ud800"] \ No newline at end of file diff --git a/resources/json/i_string_invalid_surrogate.json b/resources/json/i_string_invalid_surrogate.json new file mode 100755 index 0000000..ffddc04 --- /dev/null +++ b/resources/json/i_string_invalid_surrogate.json @@ -0,0 +1 @@ +["\ud800abc"] \ No newline at end of file diff --git a/resources/json/i_string_invalid_utf-8.json b/resources/json/i_string_invalid_utf-8.json new file mode 100644 index 0000000..8e45a7e --- /dev/null +++ b/resources/json/i_string_invalid_utf-8.json @@ -0,0 +1 @@ +["ÿ"] \ No newline at end of file diff --git a/resources/json/i_string_inverted_surrogates_U+1D11E.json b/resources/json/i_string_inverted_surrogates_U+1D11E.json new file mode 100755 index 0000000..0d5456c --- /dev/null +++ b/resources/json/i_string_inverted_surrogates_U+1D11E.json @@ -0,0 +1 @@ +["\uDd1e\uD834"] \ No newline at end of file diff --git a/resources/json/i_string_iso_latin_1.json b/resources/json/i_string_iso_latin_1.json new file mode 100644 index 0000000..9389c98 --- /dev/null +++ b/resources/json/i_string_iso_latin_1.json @@ -0,0 +1 @@ +["é"] \ No newline at end of file diff --git a/resources/json/i_string_lone_second_surrogate.json b/resources/json/i_string_lone_second_surrogate.json new file mode 100644 index 0000000..1dbd397 --- /dev/null +++ b/resources/json/i_string_lone_second_surrogate.json @@ -0,0 +1 @@ +["\uDFAA"] \ No newline at end of file diff --git a/resources/json/i_string_lone_utf8_continuation_byte.json b/resources/json/i_string_lone_utf8_continuation_byte.json new file mode 100644 index 0000000..729337c --- /dev/null +++ b/resources/json/i_string_lone_utf8_continuation_byte.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/resources/json/i_string_not_in_unicode_range.json b/resources/json/i_string_not_in_unicode_range.json new file mode 100644 index 0000000..df90a29 --- /dev/null +++ b/resources/json/i_string_not_in_unicode_range.json @@ -0,0 +1 @@ +["ô¿¿¿"] \ No newline at end of file diff --git a/resources/json/i_string_overlong_sequence_2_bytes.json b/resources/json/i_string_overlong_sequence_2_bytes.json new file mode 100644 index 0000000..c8cee5e --- /dev/null +++ b/resources/json/i_string_overlong_sequence_2_bytes.json @@ -0,0 +1 @@ +["À¯"] \ No newline at end of file diff --git a/resources/json/i_string_overlong_sequence_6_bytes.json b/resources/json/i_string_overlong_sequence_6_bytes.json new file mode 100755 index 0000000..9a91da7 --- /dev/null +++ b/resources/json/i_string_overlong_sequence_6_bytes.json @@ -0,0 +1 @@ +["üƒ¿¿¿¿"] \ No newline at end of file diff --git a/resources/json/i_string_overlong_sequence_6_bytes_null.json b/resources/json/i_string_overlong_sequence_6_bytes_null.json new file mode 100755 index 0000000..d24fffd --- /dev/null +++ b/resources/json/i_string_overlong_sequence_6_bytes_null.json @@ -0,0 +1 @@ +["ü€€€€€"] \ No newline at end of file diff --git a/resources/json/i_string_truncated-utf-8.json b/resources/json/i_string_truncated-utf-8.json new file mode 100644 index 0000000..63c7777 --- /dev/null +++ b/resources/json/i_string_truncated-utf-8.json @@ -0,0 +1 @@ +["àÿ"] \ No newline at end of file diff --git a/resources/json/i_structure_500_nested_arrays.json b/resources/json/i_structure_500_nested_arrays.json new file mode 100644 index 0000000..7118405 --- /dev/null +++ b/resources/json/i_structure_500_nested_arrays.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] \ No newline at end of file diff --git a/resources/json/y_array_arraysWithSpaces.json b/resources/json/y_array_arraysWithSpaces.json new file mode 100755 index 0000000..5822907 --- /dev/null +++ b/resources/json/y_array_arraysWithSpaces.json @@ -0,0 +1 @@ +[[] ] \ No newline at end of file diff --git a/resources/json/y_array_empty-string.json b/resources/json/y_array_empty-string.json new file mode 100644 index 0000000..93b6be2 --- /dev/null +++ b/resources/json/y_array_empty-string.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/resources/json/y_array_empty.json b/resources/json/y_array_empty.json new file mode 100755 index 0000000..0637a08 --- /dev/null +++ b/resources/json/y_array_empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/resources/json/y_array_ending_with_newline.json b/resources/json/y_array_ending_with_newline.json new file mode 100755 index 0000000..eac5f7b --- /dev/null +++ b/resources/json/y_array_ending_with_newline.json @@ -0,0 +1 @@ +["a"] \ No newline at end of file diff --git a/resources/json/y_array_false.json b/resources/json/y_array_false.json new file mode 100644 index 0000000..67b2f07 --- /dev/null +++ b/resources/json/y_array_false.json @@ -0,0 +1 @@ +[false] \ No newline at end of file diff --git a/resources/json/y_array_heterogeneous.json b/resources/json/y_array_heterogeneous.json new file mode 100755 index 0000000..d3c1e26 --- /dev/null +++ b/resources/json/y_array_heterogeneous.json @@ -0,0 +1 @@ +[null, 1, "1", {}] \ No newline at end of file diff --git a/resources/json/y_array_null.json b/resources/json/y_array_null.json new file mode 100644 index 0000000..500db4a --- /dev/null +++ b/resources/json/y_array_null.json @@ -0,0 +1 @@ +[null] \ No newline at end of file diff --git a/resources/json/y_array_with_1_and_newline.json b/resources/json/y_array_with_1_and_newline.json new file mode 100644 index 0000000..9948255 --- /dev/null +++ b/resources/json/y_array_with_1_and_newline.json @@ -0,0 +1,2 @@ +[1 +] \ No newline at end of file diff --git a/resources/json/y_array_with_leading_space.json b/resources/json/y_array_with_leading_space.json new file mode 100755 index 0000000..18bfe64 --- /dev/null +++ b/resources/json/y_array_with_leading_space.json @@ -0,0 +1 @@ + [1] \ No newline at end of file diff --git a/resources/json/y_array_with_several_null.json b/resources/json/y_array_with_several_null.json new file mode 100755 index 0000000..99f6c5d --- /dev/null +++ b/resources/json/y_array_with_several_null.json @@ -0,0 +1 @@ +[1,null,null,null,2] \ No newline at end of file diff --git a/resources/json/y_array_with_trailing_space.json b/resources/json/y_array_with_trailing_space.json new file mode 100755 index 0000000..de9e7a9 --- /dev/null +++ b/resources/json/y_array_with_trailing_space.json @@ -0,0 +1 @@ +[2] \ No newline at end of file diff --git a/resources/json/y_number.json b/resources/json/y_number.json new file mode 100644 index 0000000..e5f5cc3 --- /dev/null +++ b/resources/json/y_number.json @@ -0,0 +1 @@ +[123e65] \ No newline at end of file diff --git a/resources/json/y_number_0e+1.json b/resources/json/y_number_0e+1.json new file mode 100755 index 0000000..d1d3967 --- /dev/null +++ b/resources/json/y_number_0e+1.json @@ -0,0 +1 @@ +[0e+1] \ No newline at end of file diff --git a/resources/json/y_number_0e1.json b/resources/json/y_number_0e1.json new file mode 100755 index 0000000..3283a79 --- /dev/null +++ b/resources/json/y_number_0e1.json @@ -0,0 +1 @@ +[0e1] \ No newline at end of file diff --git a/resources/json/y_number_after_space.json b/resources/json/y_number_after_space.json new file mode 100644 index 0000000..623570d --- /dev/null +++ b/resources/json/y_number_after_space.json @@ -0,0 +1 @@ +[ 4] \ No newline at end of file diff --git a/resources/json/y_number_double_close_to_zero.json b/resources/json/y_number_double_close_to_zero.json new file mode 100755 index 0000000..96555ff --- /dev/null +++ b/resources/json/y_number_double_close_to_zero.json @@ -0,0 +1 @@ +[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001] diff --git a/resources/json/y_number_int_with_exp.json b/resources/json/y_number_int_with_exp.json new file mode 100755 index 0000000..a4ca9e7 --- /dev/null +++ b/resources/json/y_number_int_with_exp.json @@ -0,0 +1 @@ +[20e1] \ No newline at end of file diff --git a/resources/json/y_number_minus_zero.json b/resources/json/y_number_minus_zero.json new file mode 100755 index 0000000..37af131 --- /dev/null +++ b/resources/json/y_number_minus_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/resources/json/y_number_negative_int.json b/resources/json/y_number_negative_int.json new file mode 100644 index 0000000..8e30f8b --- /dev/null +++ b/resources/json/y_number_negative_int.json @@ -0,0 +1 @@ +[-123] \ No newline at end of file diff --git a/resources/json/y_number_negative_one.json b/resources/json/y_number_negative_one.json new file mode 100644 index 0000000..99d21a2 --- /dev/null +++ b/resources/json/y_number_negative_one.json @@ -0,0 +1 @@ +[-1] \ No newline at end of file diff --git a/resources/json/y_number_negative_zero.json b/resources/json/y_number_negative_zero.json new file mode 100644 index 0000000..37af131 --- /dev/null +++ b/resources/json/y_number_negative_zero.json @@ -0,0 +1 @@ +[-0] \ No newline at end of file diff --git a/resources/json/y_number_real_capital_e.json b/resources/json/y_number_real_capital_e.json new file mode 100644 index 0000000..6edbdfc --- /dev/null +++ b/resources/json/y_number_real_capital_e.json @@ -0,0 +1 @@ +[1E22] \ No newline at end of file diff --git a/resources/json/y_number_real_capital_e_neg_exp.json b/resources/json/y_number_real_capital_e_neg_exp.json new file mode 100644 index 0000000..0a01bd3 --- /dev/null +++ b/resources/json/y_number_real_capital_e_neg_exp.json @@ -0,0 +1 @@ +[1E-2] \ No newline at end of file diff --git a/resources/json/y_number_real_capital_e_pos_exp.json b/resources/json/y_number_real_capital_e_pos_exp.json new file mode 100644 index 0000000..5a8fc09 --- /dev/null +++ b/resources/json/y_number_real_capital_e_pos_exp.json @@ -0,0 +1 @@ +[1E+2] \ No newline at end of file diff --git a/resources/json/y_number_real_exponent.json b/resources/json/y_number_real_exponent.json new file mode 100644 index 0000000..da2522d --- /dev/null +++ b/resources/json/y_number_real_exponent.json @@ -0,0 +1 @@ +[123e45] \ No newline at end of file diff --git a/resources/json/y_number_real_fraction_exponent.json b/resources/json/y_number_real_fraction_exponent.json new file mode 100644 index 0000000..3944a7a --- /dev/null +++ b/resources/json/y_number_real_fraction_exponent.json @@ -0,0 +1 @@ +[123.456e78] \ No newline at end of file diff --git a/resources/json/y_number_real_neg_exp.json b/resources/json/y_number_real_neg_exp.json new file mode 100644 index 0000000..ca40d3c --- /dev/null +++ b/resources/json/y_number_real_neg_exp.json @@ -0,0 +1 @@ +[1e-2] \ No newline at end of file diff --git a/resources/json/y_number_real_pos_exponent.json b/resources/json/y_number_real_pos_exponent.json new file mode 100644 index 0000000..343601d --- /dev/null +++ b/resources/json/y_number_real_pos_exponent.json @@ -0,0 +1 @@ +[1e+2] \ No newline at end of file diff --git a/resources/json/y_number_simple_int.json b/resources/json/y_number_simple_int.json new file mode 100644 index 0000000..e47f69a --- /dev/null +++ b/resources/json/y_number_simple_int.json @@ -0,0 +1 @@ +[123] \ No newline at end of file diff --git a/resources/json/y_number_simple_real.json b/resources/json/y_number_simple_real.json new file mode 100644 index 0000000..b02878e --- /dev/null +++ b/resources/json/y_number_simple_real.json @@ -0,0 +1 @@ +[123.456789] \ No newline at end of file diff --git a/resources/json/y_object.json b/resources/json/y_object.json new file mode 100755 index 0000000..78262ed --- /dev/null +++ b/resources/json/y_object.json @@ -0,0 +1 @@ +{"asd":"sdf", "dfg":"fgh"} \ No newline at end of file diff --git a/resources/json/y_object_basic.json b/resources/json/y_object_basic.json new file mode 100755 index 0000000..646bbe7 --- /dev/null +++ b/resources/json/y_object_basic.json @@ -0,0 +1 @@ +{"asd":"sdf"} \ No newline at end of file diff --git a/resources/json/y_object_duplicated_key.json b/resources/json/y_object_duplicated_key.json new file mode 100755 index 0000000..bbc2e1c --- /dev/null +++ b/resources/json/y_object_duplicated_key.json @@ -0,0 +1 @@ +{"a":"b","a":"c"} \ No newline at end of file diff --git a/resources/json/y_object_duplicated_key_and_value.json b/resources/json/y_object_duplicated_key_and_value.json new file mode 100755 index 0000000..211581c --- /dev/null +++ b/resources/json/y_object_duplicated_key_and_value.json @@ -0,0 +1 @@ +{"a":"b","a":"b"} \ No newline at end of file diff --git a/resources/json/y_object_empty.json b/resources/json/y_object_empty.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/resources/json/y_object_empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/resources/json/y_object_empty_key.json b/resources/json/y_object_empty_key.json new file mode 100755 index 0000000..c0013d3 --- /dev/null +++ b/resources/json/y_object_empty_key.json @@ -0,0 +1 @@ +{"":0} \ No newline at end of file diff --git a/resources/json/y_object_escaped_null_in_key.json b/resources/json/y_object_escaped_null_in_key.json new file mode 100644 index 0000000..593f0f6 --- /dev/null +++ b/resources/json/y_object_escaped_null_in_key.json @@ -0,0 +1 @@ +{"foo\u0000bar": 42} \ No newline at end of file diff --git a/resources/json/y_object_extreme_numbers.json b/resources/json/y_object_extreme_numbers.json new file mode 100644 index 0000000..a0d3531 --- /dev/null +++ b/resources/json/y_object_extreme_numbers.json @@ -0,0 +1 @@ +{ "min": -1.0e+28, "max": 1.0e+28 } \ No newline at end of file diff --git a/resources/json/y_object_long_strings.json b/resources/json/y_object_long_strings.json new file mode 100644 index 0000000..bdc4a08 --- /dev/null +++ b/resources/json/y_object_long_strings.json @@ -0,0 +1 @@ +{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/resources/json/y_object_simple.json b/resources/json/y_object_simple.json new file mode 100644 index 0000000..dacac91 --- /dev/null +++ b/resources/json/y_object_simple.json @@ -0,0 +1 @@ +{"a":[]} \ No newline at end of file diff --git a/resources/json/y_object_string_unicode.json b/resources/json/y_object_string_unicode.json new file mode 100644 index 0000000..8effdb2 --- /dev/null +++ b/resources/json/y_object_string_unicode.json @@ -0,0 +1 @@ +{"title":"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430" } \ No newline at end of file diff --git a/resources/json/y_object_with_newlines.json b/resources/json/y_object_with_newlines.json new file mode 100644 index 0000000..246ec6b --- /dev/null +++ b/resources/json/y_object_with_newlines.json @@ -0,0 +1,3 @@ +{ +"a": "b" +} \ No newline at end of file diff --git a/resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json b/resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json new file mode 100755 index 0000000..9967dde --- /dev/null +++ b/resources/json/y_string_1_2_3_bytes_UTF-8_sequences.json @@ -0,0 +1 @@ +["\u0060\u012a\u12AB"] \ No newline at end of file diff --git a/resources/json/y_string_accepted_surrogate_pair.json b/resources/json/y_string_accepted_surrogate_pair.json new file mode 100755 index 0000000..996875c --- /dev/null +++ b/resources/json/y_string_accepted_surrogate_pair.json @@ -0,0 +1 @@ +["\uD801\udc37"] \ No newline at end of file diff --git a/resources/json/y_string_accepted_surrogate_pairs.json b/resources/json/y_string_accepted_surrogate_pairs.json new file mode 100755 index 0000000..3401021 --- /dev/null +++ b/resources/json/y_string_accepted_surrogate_pairs.json @@ -0,0 +1 @@ +["\ud83d\ude39\ud83d\udc8d"] \ No newline at end of file diff --git a/resources/json/y_string_allowed_escapes.json b/resources/json/y_string_allowed_escapes.json new file mode 100644 index 0000000..7f49553 --- /dev/null +++ b/resources/json/y_string_allowed_escapes.json @@ -0,0 +1 @@ +["\"\\\/\b\f\n\r\t"] \ No newline at end of file diff --git a/resources/json/y_string_backslash_and_u_escaped_zero.json b/resources/json/y_string_backslash_and_u_escaped_zero.json new file mode 100755 index 0000000..d4439ed --- /dev/null +++ b/resources/json/y_string_backslash_and_u_escaped_zero.json @@ -0,0 +1 @@ +["\\u0000"] \ No newline at end of file diff --git a/resources/json/y_string_backslash_doublequotes.json b/resources/json/y_string_backslash_doublequotes.json new file mode 100644 index 0000000..ae03243 --- /dev/null +++ b/resources/json/y_string_backslash_doublequotes.json @@ -0,0 +1 @@ +["\""] \ No newline at end of file diff --git a/resources/json/y_string_comments.json b/resources/json/y_string_comments.json new file mode 100644 index 0000000..2260c20 --- /dev/null +++ b/resources/json/y_string_comments.json @@ -0,0 +1 @@ +["a/*b*/c/*d//e"] \ No newline at end of file diff --git a/resources/json/y_string_double_escape_a.json b/resources/json/y_string_double_escape_a.json new file mode 100644 index 0000000..6715d6f --- /dev/null +++ b/resources/json/y_string_double_escape_a.json @@ -0,0 +1 @@ +["\\a"] \ No newline at end of file diff --git a/resources/json/y_string_double_escape_n.json b/resources/json/y_string_double_escape_n.json new file mode 100644 index 0000000..44ca56c --- /dev/null +++ b/resources/json/y_string_double_escape_n.json @@ -0,0 +1 @@ +["\\n"] \ No newline at end of file diff --git a/resources/json/y_string_escaped_control_character.json b/resources/json/y_string_escaped_control_character.json new file mode 100644 index 0000000..5b014a9 --- /dev/null +++ b/resources/json/y_string_escaped_control_character.json @@ -0,0 +1 @@ +["\u0012"] \ No newline at end of file diff --git a/resources/json/y_string_escaped_noncharacter.json b/resources/json/y_string_escaped_noncharacter.json new file mode 100755 index 0000000..2ff52e2 --- /dev/null +++ b/resources/json/y_string_escaped_noncharacter.json @@ -0,0 +1 @@ +["\uFFFF"] \ No newline at end of file diff --git a/resources/json/y_string_in_array.json b/resources/json/y_string_in_array.json new file mode 100755 index 0000000..21d7ae4 --- /dev/null +++ b/resources/json/y_string_in_array.json @@ -0,0 +1 @@ +["asd"] \ No newline at end of file diff --git a/resources/json/y_string_in_array_with_leading_space.json b/resources/json/y_string_in_array_with_leading_space.json new file mode 100755 index 0000000..9e1887c --- /dev/null +++ b/resources/json/y_string_in_array_with_leading_space.json @@ -0,0 +1 @@ +[ "asd"] \ No newline at end of file diff --git a/resources/json/y_string_last_surrogates_1_and_2.json b/resources/json/y_string_last_surrogates_1_and_2.json new file mode 100644 index 0000000..3919cef --- /dev/null +++ b/resources/json/y_string_last_surrogates_1_and_2.json @@ -0,0 +1 @@ +["\uDBFF\uDFFF"] \ No newline at end of file diff --git a/resources/json/y_string_nbsp_uescaped.json b/resources/json/y_string_nbsp_uescaped.json new file mode 100644 index 0000000..2085ab1 --- /dev/null +++ b/resources/json/y_string_nbsp_uescaped.json @@ -0,0 +1 @@ +["new\u00A0line"] \ No newline at end of file diff --git a/resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json b/resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json new file mode 100755 index 0000000..059e4d9 --- /dev/null +++ b/resources/json/y_string_nonCharacterInUTF-8_U+10FFFF.json @@ -0,0 +1 @@ +["ô¿¿"] \ No newline at end of file diff --git a/resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json b/resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json new file mode 100755 index 0000000..4c913bd --- /dev/null +++ b/resources/json/y_string_nonCharacterInUTF-8_U+FFFF.json @@ -0,0 +1 @@ +["ï¿¿"] \ No newline at end of file diff --git a/resources/json/y_string_null_escape.json b/resources/json/y_string_null_escape.json new file mode 100644 index 0000000..c1ad844 --- /dev/null +++ b/resources/json/y_string_null_escape.json @@ -0,0 +1 @@ +["\u0000"] \ No newline at end of file diff --git a/resources/json/y_string_one-byte-utf-8.json b/resources/json/y_string_one-byte-utf-8.json new file mode 100644 index 0000000..1571859 --- /dev/null +++ b/resources/json/y_string_one-byte-utf-8.json @@ -0,0 +1 @@ +["\u002c"] \ No newline at end of file diff --git a/resources/json/y_string_pi.json b/resources/json/y_string_pi.json new file mode 100644 index 0000000..9df11ae --- /dev/null +++ b/resources/json/y_string_pi.json @@ -0,0 +1 @@ +["Ï€"] \ No newline at end of file diff --git a/resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json b/resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json new file mode 100755 index 0000000..10a33a1 --- /dev/null +++ b/resources/json/y_string_reservedCharacterInUTF-8_U+1BFFF.json @@ -0,0 +1 @@ +["𛿿"] \ No newline at end of file diff --git a/resources/json/y_string_simple_ascii.json b/resources/json/y_string_simple_ascii.json new file mode 100644 index 0000000..8cadf7d --- /dev/null +++ b/resources/json/y_string_simple_ascii.json @@ -0,0 +1 @@ +["asd "] \ No newline at end of file diff --git a/resources/json/y_string_space.json b/resources/json/y_string_space.json new file mode 100644 index 0000000..efd782c --- /dev/null +++ b/resources/json/y_string_space.json @@ -0,0 +1 @@ +" " \ No newline at end of file diff --git a/resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json b/resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json new file mode 100755 index 0000000..7620b66 --- /dev/null +++ b/resources/json/y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json @@ -0,0 +1 @@ +["\uD834\uDd1e"] \ No newline at end of file diff --git a/resources/json/y_string_three-byte-utf-8.json b/resources/json/y_string_three-byte-utf-8.json new file mode 100644 index 0000000..108f1d6 --- /dev/null +++ b/resources/json/y_string_three-byte-utf-8.json @@ -0,0 +1 @@ +["\u0821"] \ No newline at end of file diff --git a/resources/json/y_string_two-byte-utf-8.json b/resources/json/y_string_two-byte-utf-8.json new file mode 100644 index 0000000..461503c --- /dev/null +++ b/resources/json/y_string_two-byte-utf-8.json @@ -0,0 +1 @@ +["\u0123"] \ No newline at end of file diff --git a/resources/json/y_string_u+2028_line_sep.json b/resources/json/y_string_u+2028_line_sep.json new file mode 100755 index 0000000..897b602 --- /dev/null +++ b/resources/json/y_string_u+2028_line_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/resources/json/y_string_u+2029_par_sep.json b/resources/json/y_string_u+2029_par_sep.json new file mode 100755 index 0000000..8cd998c --- /dev/null +++ b/resources/json/y_string_u+2029_par_sep.json @@ -0,0 +1 @@ +["
"] \ No newline at end of file diff --git a/resources/json/y_string_uEscape.json b/resources/json/y_string_uEscape.json new file mode 100755 index 0000000..f7b41a0 --- /dev/null +++ b/resources/json/y_string_uEscape.json @@ -0,0 +1 @@ +["\u0061\u30af\u30EA\u30b9"] \ No newline at end of file diff --git a/resources/json/y_string_uescaped_newline.json b/resources/json/y_string_uescaped_newline.json new file mode 100644 index 0000000..3a5a220 --- /dev/null +++ b/resources/json/y_string_uescaped_newline.json @@ -0,0 +1 @@ +["new\u000Aline"] \ No newline at end of file diff --git a/resources/json/y_string_unescaped_char_delete.json b/resources/json/y_string_unescaped_char_delete.json new file mode 100755 index 0000000..7d064f4 --- /dev/null +++ b/resources/json/y_string_unescaped_char_delete.json @@ -0,0 +1 @@ +[""] \ No newline at end of file diff --git a/resources/json/y_string_unicode.json b/resources/json/y_string_unicode.json new file mode 100644 index 0000000..3598095 --- /dev/null +++ b/resources/json/y_string_unicode.json @@ -0,0 +1 @@ +["\uA66D"] \ No newline at end of file diff --git a/resources/json/y_string_unicodeEscapedBackslash.json b/resources/json/y_string_unicodeEscapedBackslash.json new file mode 100755 index 0000000..0bb3b51 --- /dev/null +++ b/resources/json/y_string_unicodeEscapedBackslash.json @@ -0,0 +1 @@ +["\u005C"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_2.json b/resources/json/y_string_unicode_2.json new file mode 100644 index 0000000..a7dcb97 --- /dev/null +++ b/resources/json/y_string_unicode_2.json @@ -0,0 +1 @@ +["â‚㈴â‚"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+10FFFE_nonchar.json b/resources/json/y_string_unicode_U+10FFFE_nonchar.json new file mode 100644 index 0000000..9a8370b --- /dev/null +++ b/resources/json/y_string_unicode_U+10FFFE_nonchar.json @@ -0,0 +1 @@ +["\uDBFF\uDFFE"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+1FFFE_nonchar.json b/resources/json/y_string_unicode_U+1FFFE_nonchar.json new file mode 100644 index 0000000..c51f8ae --- /dev/null +++ b/resources/json/y_string_unicode_U+1FFFE_nonchar.json @@ -0,0 +1 @@ +["\uD83F\uDFFE"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json b/resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json new file mode 100644 index 0000000..626d5f8 --- /dev/null +++ b/resources/json/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json @@ -0,0 +1 @@ +["\u200B"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+2064_invisible_plus.json b/resources/json/y_string_unicode_U+2064_invisible_plus.json new file mode 100644 index 0000000..1e23972 --- /dev/null +++ b/resources/json/y_string_unicode_U+2064_invisible_plus.json @@ -0,0 +1 @@ +["\u2064"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+FDD0_nonchar.json b/resources/json/y_string_unicode_U+FDD0_nonchar.json new file mode 100644 index 0000000..18ef151 --- /dev/null +++ b/resources/json/y_string_unicode_U+FDD0_nonchar.json @@ -0,0 +1 @@ +["\uFDD0"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_U+FFFE_nonchar.json b/resources/json/y_string_unicode_U+FFFE_nonchar.json new file mode 100644 index 0000000..13d261f --- /dev/null +++ b/resources/json/y_string_unicode_U+FFFE_nonchar.json @@ -0,0 +1 @@ +["\uFFFE"] \ No newline at end of file diff --git a/resources/json/y_string_unicode_escaped_double_quote.json b/resources/json/y_string_unicode_escaped_double_quote.json new file mode 100755 index 0000000..4e62578 --- /dev/null +++ b/resources/json/y_string_unicode_escaped_double_quote.json @@ -0,0 +1 @@ +["\u0022"] \ No newline at end of file diff --git a/resources/json/y_string_utf8.json b/resources/json/y_string_utf8.json new file mode 100644 index 0000000..4087843 --- /dev/null +++ b/resources/json/y_string_utf8.json @@ -0,0 +1 @@ +["€ð„ž"] \ No newline at end of file diff --git a/resources/json/y_string_with_del_character.json b/resources/json/y_string_with_del_character.json new file mode 100755 index 0000000..8bd2490 --- /dev/null +++ b/resources/json/y_string_with_del_character.json @@ -0,0 +1 @@ +["aa"] \ No newline at end of file diff --git a/resources/json/y_structure_lonely_false.json b/resources/json/y_structure_lonely_false.json new file mode 100644 index 0000000..02e4a84 --- /dev/null +++ b/resources/json/y_structure_lonely_false.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/resources/json/y_structure_lonely_int.json b/resources/json/y_structure_lonely_int.json new file mode 100755 index 0000000..f70d7bb --- /dev/null +++ b/resources/json/y_structure_lonely_int.json @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/resources/json/y_structure_lonely_negative_real.json b/resources/json/y_structure_lonely_negative_real.json new file mode 100755 index 0000000..b5135a2 --- /dev/null +++ b/resources/json/y_structure_lonely_negative_real.json @@ -0,0 +1 @@ +-0.1 \ No newline at end of file diff --git a/resources/json/y_structure_lonely_null.json b/resources/json/y_structure_lonely_null.json new file mode 100644 index 0000000..ec747fa --- /dev/null +++ b/resources/json/y_structure_lonely_null.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/resources/json/y_structure_lonely_string.json b/resources/json/y_structure_lonely_string.json new file mode 100755 index 0000000..b6e982c --- /dev/null +++ b/resources/json/y_structure_lonely_string.json @@ -0,0 +1 @@ +"asd" \ No newline at end of file diff --git a/resources/json/y_structure_lonely_true.json b/resources/json/y_structure_lonely_true.json new file mode 100755 index 0000000..f32a580 --- /dev/null +++ b/resources/json/y_structure_lonely_true.json @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/resources/json/y_structure_string_empty.json b/resources/json/y_structure_string_empty.json new file mode 100644 index 0000000..3cc762b --- /dev/null +++ b/resources/json/y_structure_string_empty.json @@ -0,0 +1 @@ +"" \ No newline at end of file diff --git a/resources/json/y_structure_trailing_newline.json b/resources/json/y_structure_trailing_newline.json new file mode 100644 index 0000000..0c3426d --- /dev/null +++ b/resources/json/y_structure_trailing_newline.json @@ -0,0 +1 @@ +["a"] diff --git a/resources/json/y_structure_true_in_array.json b/resources/json/y_structure_true_in_array.json new file mode 100644 index 0000000..de601e3 --- /dev/null +++ b/resources/json/y_structure_true_in_array.json @@ -0,0 +1 @@ +[true] \ No newline at end of file diff --git a/resources/json/y_structure_whitespace_array.json b/resources/json/y_structure_whitespace_array.json new file mode 100644 index 0000000..2bedf7f --- /dev/null +++ b/resources/json/y_structure_whitespace_array.json @@ -0,0 +1 @@ + [] \ No newline at end of file diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs index 188d3c3..1085e6d 100644 --- a/src/server/refresh/docker.rs +++ b/src/server/refresh/docker.rs @@ -87,6 +87,20 @@ enum JsonValue { Array(Vec), } +/// If the characters at `chars` match the characters in `prefix`, consume +/// those and return true. Otherwise, return false and do not advance the +/// iterator. +fn matches(chars: &mut std::str::Chars, prefix: &str) -> bool { + let backup = chars.clone(); + for c in prefix.chars() { + if chars.next() != Some(c) { + *chars = backup; + return false; + } + } + true +} + impl JsonValue { pub fn parse(blob: &[u8]) -> Result { Self::parse_impl(blob).with_context(|| { @@ -191,8 +205,9 @@ impl JsonValue { bail!("Unterminated string at {i}"); } assert_eq!(blob[i], b'"'); - let mut chars = - std::str::from_utf8(&blob[start..i])?.chars(); + + let source = String::from_utf8_lossy(&blob[start..i]); + let mut chars = source.chars(); i += 1; // Consume the final quote. let mut value = String::new(); @@ -201,25 +216,52 @@ impl JsonValue { match chars.next().expect("mismatched escape") { '"' => value.push('"'), '\\' => value.push('\\'), + '/' => value.push('/'), 'b' => value.push('\x08'), 'f' => value.push('\x0C'), 'n' => value.push('\n'), 'r' => value.push('\r'), 't' => value.push('\t'), 'u' => { - // 4 hex + // 4 hex to a 16bit number. + // + // This is complicated because it might + // be the first part of a surrogate pair, + // which is a utf-16 thing that we should + // decode properly. (Hard to think of an + // acceptable way to cheat this.) So we + // buffer all codes we can find into a + // vector of u16s and then use + // std::char's `decode_utf16` to get the + // final string. We could do this with + // fewer allocations if we cared more. let mut temp = String::with_capacity(4); - for _ in 0..4 { - let Some(c) = chars.next() else { - bail!("not enough chars in unicode escape") - }; - temp.push(c); + let mut utf16 = Vec::new(); + loop { + temp.clear(); + for _ in 0..4 { + let Some(c) = chars.next() else { + bail!("not enough chars in unicode escape") + }; + temp.push(c); + } + let code = + u16::from_str_radix(&temp, 16)?; + utf16.push(code); + + // `matches` only consumes on a match... + if !matches(&mut chars, "\\u") { + break; + } } - let code = u32::from_str_radix(&temp, 16)?; - let Some(c) = char::from_u32(code) else { - bail!("invalid escape code {temp}") - }; - value.push(c); + + value.extend( + char::decode_utf16(utf16).map(|r| { + r.unwrap_or( + char::REPLACEMENT_CHARACTER, + ) + }), + ); } _ => bail!("Invalid json escape"), } @@ -348,7 +390,7 @@ mod test { use super::*; #[test] - pub fn test_json_decode_basic() { + pub fn json_decode_basic() { let cases: Vec<(&str, JsonValue)> = vec![ ("12", JsonValue::Number(12.0)), ("12.7", JsonValue::Number(12.7)), @@ -370,7 +412,7 @@ mod test { } #[test] - pub fn test_json_decode_array() { + pub fn json_decode_array() { let result = JsonValue::parse(b"[1, true, \"foo\", null]").unwrap(); let JsonValue::Array(result) = result else { panic!("Expected an array"); @@ -383,13 +425,13 @@ mod test { } #[test] - pub fn test_json_decode_array_empty() { + pub fn json_decode_array_empty() { let result = JsonValue::parse(b"[]").unwrap(); assert_eq!(result, JsonValue::Array(vec![])); } #[test] - pub fn test_json_decode_array_nested() { + pub fn json_decode_array_nested() { let result = JsonValue::parse(b"[1, [2, 3], 4]").unwrap(); assert_eq!( result, @@ -405,7 +447,7 @@ mod test { } #[test] - pub fn test_json_decode_object() { + pub fn json_decode_object() { let result = JsonValue::parse( b"{\"a\": 1.0, \"b\": [2.0, 3.0], \"c\": {\"d\": 4.0}}", ) @@ -433,7 +475,34 @@ mod test { } #[test] - pub fn test_json_decode_docker() { + pub fn json_decode_test_files() { + use std::path::{Path, PathBuf}; + fn is_json(p: &Path) -> bool { + p.is_file() && p.extension().map(|s| s == "json").unwrap_or(false) + } + + let manifest_dir: PathBuf = + [env!("CARGO_MANIFEST_DIR"), "resources", "json"] + .iter() + .collect(); + + for file in manifest_dir.read_dir().unwrap().flatten() { + let path = file.path(); + if !is_json(&path) { + continue; + } + + let json = std::fs::read(&path).expect("Unable to read input file"); + let path = path.display(); + if let Err(err) = JsonValue::parse(&json) { + panic!("Unable to parse {path}: {err:?}"); + } + eprintln!("Parsed {path} successfully"); + } + } + + #[test] + pub fn json_decode_docker() { use pretty_assertions::assert_eq; // This is the example container response from docker From b8fe678ff057c647f4971465ad8b15a6d9f8c8d1 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 7 Aug 2024 12:14:04 -0700 Subject: [PATCH 32/85] Repository information in version --- build.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 32 +++++++++++++--- 2 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 build.rs diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b33bf68 --- /dev/null +++ b/build.rs @@ -0,0 +1,108 @@ +use std::io::Write; +use std::path::{absolute, Path, PathBuf}; + +/// Fetch the contents of the given file, and also tell cargo that we looked +/// in there. +fn file_contents>(path: P) -> String { + let path = + absolute(path.as_ref()).expect("Unable to make the path absolute"); + + let mut stdout = std::io::stdout(); + stdout + .write_all(b"cargo::rerun-if-changed=") + .expect("Unable to write stdout"); + stdout + .write_all(path.as_os_str().as_encoded_bytes()) + .expect("Unable to write path to stdout"); + stdout + .write_all(b"\n") + .expect("Unable to write newline to stdout"); + + std::fs::read_to_string(path).expect("Unable to read file") +} + +/// Emit the current git commit. +fn emit_git_commit() { + // Fetch the current commit from the head. We do it this way instead of + // asking `git rev-parse` to do it for us because we want to reliably + // tell cargo which files it should monitor for changes. + let head = file_contents("./.git/HEAD"); + let rev = if let Some(r) = head.strip_prefix("ref: ") { + let mut ref_path = PathBuf::from("./.git/"); + ref_path.push(r.trim()); + file_contents(ref_path) + } else { + head + }; + + // But *now* we ask git rev-parse to make this into a short hash (a) to + // make sure we got it right and (b) because git knows how to quickly + // determine how much of a commit is required to be unique. We don't need + // to tell cargo anything here, no file that git consults will be + // mutable. + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg("--short") + .arg(rev.trim()) + .output() + .expect("could not spawn `git` to get the hash"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf8"); + eprintln!("`git rev-parse --short HEAD` failed, stderr: {stderr}"); + panic!("`git rev-parse --short HEAD` failed"); + } + let rev = + std::str::from_utf8(&output.stdout).expect("git did not output utf8"); + + println!("cargo::rustc-env=REPO_REV={rev}"); +} + +fn emit_git_dirty() { + // Here is the way to see if anything is up with the repository: run `git + // status --porcelain=v1`. The status output in the v1 porcelain format + // has one line for every file that's modified in some way: staged, + // changed but unstaged, untracked, you name it. Files in the working + // tree that are up to date with the repository are not emitted. This is + // exactly what we want. + // + // (Yes, I want to track untracked files, because they can mess with the + // build too. The only good build is a clean build!) + let output = std::process::Command::new("git") + .arg("status") + .arg("-z") + .arg("--porcelain=v1") + .output() + .expect("could not spawn `git` to get repository status"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf8"); + eprintln!("`git status` failed, stderr: {stderr}"); + panic!("`git status` failed"); + } + let output = + std::str::from_utf8(&output.stdout).expect("git did not output utf8"); + + // If there *was* any output, parse it and tell cargo to re-run if any of + // these files changed. (Maybe they get reverted! Then the repo status + // will change.) + for line in output.lines() { + let fields: Vec<_> = line.split('\x00').collect(); + let parts: Vec<_> = fields[0].split(' ').collect(); + let path = parts[1]; + println!("cargo::rerun-if-changed={path}"); + } + + // Emit the repository status. + let dirty = if output.trim().is_empty() { + "" + } else { + " *dirty*" + }; + println!("cargo::rustc-env=REPO_DIRTY={dirty}"); +} + +fn main() { + emit_git_commit(); + emit_git_dirty(); +} diff --git a/src/main.rs b/src/main.rs index 453c78d..6862124 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ use indoc::indoc; const VERSION: &str = env!("CARGO_PKG_VERSION"); +const REV: &str = env!("REPO_REV"); +const DIRTY: &str = env!("REPO_DIRTY"); fn usage() { println!(indoc! {" @@ -85,13 +87,21 @@ fn parse_args(args: Vec) -> Args { if rest.len() == 2 { Args::Browse(rest[1].to_string()) } else if rest.len() == 1 { - Args::Client(rest[0].to_string(), sudo.unwrap_or(false), log_filter.unwrap_or("warn".to_owned())) + Args::Client( + rest[0].to_string(), + sudo.unwrap_or(false), + log_filter.unwrap_or("warn".to_owned()), + ) } else { Args::Error } } else if rest[0] == "clip" { if rest.len() == 1 { - Args::Client(rest[0].to_string(), sudo.unwrap_or(false), log_filter.unwrap_or("warn".to_owned())) + Args::Client( + rest[0].to_string(), + sudo.unwrap_or(false), + log_filter.unwrap_or("warn".to_owned()), + ) } else if rest.len() == 2 { Args::Clip(rest[1].to_string()) } else { @@ -131,7 +141,7 @@ async fn main() { usage(); } Args::Version => { - println!("fwd {VERSION}"); + println!("fwd {VERSION} (rev {REV}{DIRTY})"); } Args::Server => { fwd::run_server().await; @@ -193,7 +203,7 @@ mod tests { #[test] fn client() { - assert_arg_parse!(&["foo.com"], Args::Client( _, false, _)); + assert_arg_parse!(&["foo.com"], Args::Client(_, false, _)); assert_arg_parse!(&["a"], Args::Client(_, false, _)); assert_arg_parse!(&["browse"], Args::Client(_, false, _)); assert_arg_parse!(&["clip"], Args::Client(_, false, _)); @@ -205,10 +215,20 @@ mod tests { assert_client_parse(&["a"], "a", false, "warn"); assert_client_parse(&["a", "--log-filter", "info"], "a", false, "info"); assert_client_parse(&["a", "--log-filter=info"], "a", false, "info"); - assert_client_parse(&["a", "--sudo", "--log-filter=info"], "a", true, "info"); + assert_client_parse( + &["a", "--sudo", "--log-filter=info"], + "a", + true, + "info", + ); } - fn assert_client_parse(x: &[&str], server: &str, sudo: bool, log_filter: &str) { + fn assert_client_parse( + x: &[&str], + server: &str, + sudo: bool, + log_filter: &str, + ) { let args = parse_args(args(x)); assert_matches!(args, Args::Client(_, _, _)); if let Args::Client(s, sdo, lf) = args { From a3fa03250075eee3443e70d0b64bf0b80c2b2cde Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 07:18:13 -0700 Subject: [PATCH 33/85] Fix git status parsing --- build.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/build.rs b/build.rs index b33bf68..ed759f1 100644 --- a/build.rs +++ b/build.rs @@ -52,8 +52,9 @@ fn emit_git_commit() { eprintln!("`git rev-parse --short HEAD` failed, stderr: {stderr}"); panic!("`git rev-parse --short HEAD` failed"); } - let rev = - std::str::from_utf8(&output.stdout).expect("git did not output utf8"); + let rev = std::str::from_utf8(&output.stdout) + .expect("git did not output utf8") + .trim(); println!("cargo::rustc-env=REPO_REV={rev}"); } @@ -86,13 +87,20 @@ fn emit_git_dirty() { // If there *was* any output, parse it and tell cargo to re-run if any of // these files changed. (Maybe they get reverted! Then the repo status // will change.) - for line in output.lines() { - let fields: Vec<_> = line.split('\x00').collect(); - let parts: Vec<_> = fields[0].split(' ').collect(); - let path = parts[1]; - println!("cargo::rerun-if-changed={path}"); + let mut split = output.split('\x00'); + while let Some(field) = split.next() { + if field.is_empty() { + continue; + } + let prefix = &field[0..3]; + println!("cargo::rerun-if-changed={}", &field[3..]); + let b = prefix.as_bytes(); + if b[0] == b'R' || b[1] == b'R' || b[0] == b'C' || b[1] == b'C' { + if let Some(additional) = split.next() { + println!("cargo::rerun-if-changed={additional}"); + } + } } - // Emit the repository status. let dirty = if output.trim().is_empty() { "" From 2a582e25a83f0baae171a488da0349ce441add0b Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 10:03:47 -0700 Subject: [PATCH 34/85] Many fixes for the clipboard and others - Reverse connections must be maintained to ensure messages are processed in order. (Whoops!) - The clipboard context must remain live in order for the data to remain available for applications, at least on X11. (And it couldn't hurt elsewhere, either, I guess.) - Print out the server version at startup time, so we can be sure what we're talking to. - Print out the full details of the error when something goes wrong with `browse` or `clip`. --- src/client/mod.rs | 13 +++----- src/client/ui.rs | 14 ++++++++- src/lib.rs | 4 +++ src/main.rs | 10 ++----- src/reverse.rs | 38 +++++++++++++++++------- src/reverse/unix.rs | 72 +++++++++++++++++++++++++++------------------ src/server/mod.rs | 16 +++++++++- 7 files changed, 110 insertions(+), 57 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 18bd3fc..94cb208 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,7 +1,6 @@ use crate::message::{Message, MessageReader, MessageWriter}; use anyhow::{bail, Result}; use bytes::BytesMut; -use copypasta::{ClipboardContext, ClipboardProvider}; use log::LevelFilter; use log::{debug, error, info, warn}; use std::collections::HashMap; @@ -213,16 +212,11 @@ async fn client_handle_messages( } ClipStart(id) => { - info!("Starting clip op {id}"); clipboard_messages.insert(id, Vec::new()); } ClipData(id, mut data) => match clipboard_messages.get_mut(&id) { Some(bytes) => { - info!( - "Received data for clip op {id} ({len} bytes)", - len = data.len() - ); if bytes.len() < MAX_CLIPBOARD_SIZE { bytes.append(&mut data); } @@ -243,9 +237,10 @@ async fn client_handle_messages( continue; }; - let mut ctx = ClipboardContext::new().unwrap(); - if let Err(e) = ctx.set_contents(data) { - error!("Unable to set clipboard data for op {id}: {e:?}"); + if let Err(e) = + events.send(ui::UIEvent::SetClipboard(data)).await + { + error!("Error sending clipboard request: {:?}", e); } } diff --git a/src/client/ui.rs b/src/client/ui.rs index a20a8e1..923d731 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -1,6 +1,7 @@ use super::{client_listen, config::ServerConfig}; use crate::message::PortDesc; use anyhow::Result; +use copypasta::{ClipboardContext, ClipboardProvider}; use crossterm::{ event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, execute, @@ -33,6 +34,7 @@ pub enum UIEvent { ServerLine(String), LogLine(log::Level, String), Ports(Vec), + SetClipboard(String), } pub enum UIReturn { @@ -166,7 +168,6 @@ impl Listener { } } -#[derive(Debug)] pub struct UI { events: mpsc::Receiver, ports: HashMap, @@ -179,6 +180,7 @@ pub struct UI { show_help: bool, alternate_screen: bool, raw_mode: bool, + clipboard: ClipboardContext, } impl UI { @@ -195,6 +197,8 @@ impl UI { config, alternate_screen: false, raw_mode: false, + clipboard: ClipboardContext::new() + .expect("Unable to initialize clipboard context"), } } @@ -621,6 +625,14 @@ impl UI { } self.lines.push_back(format!("[CLIENT] {line}")); } + Some(UIEvent::SetClipboard(contents)) => { + let length = contents.len(); + if let Err(e) = self.clipboard.set_contents(contents) { + error!("Error setting clipboard contents: {e:#}"); + } else { + info!("Received clipboard contents ({length} bytes)"); + } + } None => { self.running = false; } diff --git a/src/lib.rs b/src/lib.rs index a1e99a7..6920aa0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,10 @@ mod message; mod reverse; mod server; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const REV: &str = env!("REPO_REV"); +pub const DIRTY: &str = env!("REPO_DIRTY"); + pub use client::run_client; pub use reverse::browse_url; pub use reverse::clip_file; diff --git a/src/main.rs b/src/main.rs index 6862124..7871d50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,6 @@ // TODO: An actual proper command line parsing use indoc::indoc; -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const REV: &str = env!("REPO_REV"); -const DIRTY: &str = env!("REPO_DIRTY"); - fn usage() { println!(indoc! {" usage: fwd [options] ( | browse | clip []) @@ -121,7 +117,7 @@ fn parse_args(args: Vec) -> Args { async fn browse_url(url: &str) { if let Err(e) = fwd::browse_url(url).await { eprintln!("Unable to open {url}"); - eprintln!("{}", e); + eprintln!("{:#}", e); std::process::exit(1); } } @@ -129,7 +125,7 @@ async fn browse_url(url: &str) { async fn clip_file(file: String) { if let Err(e) = fwd::clip_file(&file).await { eprintln!("Unable to copy to the clipboard"); - eprintln!("{}", e); + eprintln!("{:#}", e); std::process::exit(1); } } @@ -141,7 +137,7 @@ async fn main() { usage(); } Args::Version => { - println!("fwd {VERSION} (rev {REV}{DIRTY})"); + println!("fwd {} (rev {}{})", fwd::VERSION, fwd::REV, fwd::DIRTY); } Args::Server => { fwd::run_server().await; diff --git a/src/reverse.rs b/src/reverse.rs index a7d8534..f717dd9 100644 --- a/src/reverse.rs +++ b/src/reverse.rs @@ -6,16 +6,28 @@ use tokio::io::{AsyncRead, AsyncReadExt}; mod unix; #[cfg(target_family = "unix")] -pub use unix::{handle_reverse_connections, send_reverse_message}; +pub use unix::{handle_reverse_connections, ReverseConnection}; use crate::message::Message; #[cfg(not(target_family = "unix"))] -pub async fn send_reverse_message(_message: Message) -> Result<()> { - use anyhow::anyhow; - Err(anyhow!( - "Server-side operations are not supported on this platform" - )) +pub struct ReverseConnection {} + +#[cfg(not(target_family = "unix"))] +impl ReverseConnection { + pub async fn new() -> Result { + use anyhow::anyhow; + Err(anyhow!( + "Server-side operations are not supported on this platform" + )) + } + + pub async fn send(&mut self, message: Message) -> Result<()> { + use anyhow::anyhow; + Err(anyhow!( + "Server-side operations are not supported on this platform" + )) + } } #[cfg(not(target_family = "unix"))] @@ -27,12 +39,16 @@ pub async fn handle_reverse_connections( #[inline] pub async fn browse_url(url: &str) -> Result<()> { - send_reverse_message(Message::Browse(url.to_string())).await + ReverseConnection::new() + .await? + .send(Message::Browse(url.to_string())) + .await } async fn clip_reader(reader: &mut T) -> Result<()> { + let mut connection = ReverseConnection::new().await?; let clip_id: u64 = random(); - send_reverse_message(Message::ClipStart(clip_id)).await?; + connection.send(Message::ClipStart(clip_id)).await?; let mut count = 0; let mut buf = vec![0; 1024]; @@ -43,7 +59,7 @@ async fn clip_reader(reader: &mut T) -> Result<()> { } count += read; if count == buf.len() { - send_reverse_message(Message::ClipData(clip_id, buf)).await?; + connection.send(Message::ClipData(clip_id, buf)).await?; buf = vec![0; 1024]; count = 0; } @@ -51,10 +67,10 @@ async fn clip_reader(reader: &mut T) -> Result<()> { if count > 0 { buf.resize(count, 0); - send_reverse_message(Message::ClipData(clip_id, buf)).await?; + connection.send(Message::ClipData(clip_id, buf)).await?; } - send_reverse_message(Message::ClipEnd(clip_id)).await?; + connection.send(Message::ClipEnd(clip_id)).await?; Ok(()) } diff --git a/src/reverse/unix.rs b/src/reverse/unix.rs index 92025ce..d3cc801 100644 --- a/src/reverse/unix.rs +++ b/src/reverse/unix.rs @@ -9,32 +9,27 @@ use tokio::sync::mpsc; use crate::message::{Message, MessageReader, MessageWriter}; -pub async fn send_reverse_message(message: Message) -> Result<()> { - let path = socket_path().context("Error getting socket path")?; - let stream = match UnixStream::connect(&path).await { - Ok(s) => s, - Err(e) => bail!( - "Error connecting to socket: {e} (is fwd actually connected here?)" - ), - }; - let mut writer = MessageWriter::new(stream); - writer - .write(message) - .await - .context("Error sending browse message")?; - Ok(()) +pub struct ReverseConnection { + writer: MessageWriter, } -fn socket_directory() -> Result { - let base_directories = xdg::BaseDirectories::new() - .context("Error creating BaseDirectories")?; - match base_directories.place_runtime_file("fwd") { - Ok(path) => Ok(path), - Err(_) => { - let mut path = std::env::temp_dir(); - path.push(format!("fwd{}", users::get_current_uid())); - Ok(path) - } +impl ReverseConnection { + pub async fn new() -> Result { + let path = socket_path().context("Error getting socket path")?; + let stream = match UnixStream::connect(&path).await { + Ok(s) => s, + Err(e) => bail!("Error connecting to socket: {e} (is fwd actually connected here?)"), + }; + + Ok(ReverseConnection { writer: MessageWriter::new(stream) }) + } + + pub async fn send(&mut self, message: Message) -> Result<()> { + self.writer + .write(message) + .await + .context("Error sending reverse message")?; + Ok(()) } } @@ -53,6 +48,19 @@ pub fn socket_path() -> Result { Ok(socket_path) } +fn socket_directory() -> Result { + let base_directories = xdg::BaseDirectories::new() + .context("Error creating BaseDirectories")?; + match base_directories.place_runtime_file("fwd") { + Ok(path) => Ok(path), + Err(_) => { + let mut path = std::env::temp_dir(); + path.push(format!("fwd{}", users::get_current_uid())); + Ok(path) + } + } +} + pub async fn handle_reverse_connections( messages: mpsc::Sender, ) -> Result<()> { @@ -87,10 +95,18 @@ async fn handle_connection( sender: mpsc::Sender, ) -> Result<()> { let mut reader = MessageReader::new(socket); - let message = reader.read().await.context("Error reading message")?; - match message { - Message::Browse(url) => sender.send(Message::Browse(url)).await?, - _ => bail!("Unsupported message: {:?}", message), + while let Ok(message) = reader.read().await { + match message { + Message::Browse(url) => sender.send(Message::Browse(url)).await?, + Message::ClipStart(id) => { + sender.send(Message::ClipStart(id)).await? + } + Message::ClipData(id, data) => { + sender.send(Message::ClipData(id, data)).await? + } + Message::ClipEnd(id) => sender.send(Message::ClipEnd(id)).await?, + _ => bail!("Unsupported message: {:?}", message), + } } Ok(()) diff --git a/src/server/mod.rs b/src/server/mod.rs index 1188c98..804d877 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -26,12 +26,26 @@ async fn server_loop( ) -> Result<()> { // The first message we send must be an announcement. writer.send(Message::Hello(0, 2, vec![])).await?; - + let mut version_reported = false; loop { use Message::*; match reader.read().await? { Ping => (), Refresh => { + // Just log the version, if we haven't yet. We do this extra + // work to avoid spamming the log, but we wait until we + // receive the first message to be sure that the client is in + // a place to display our logging properly. + if !version_reported { + eprintln!( + "fwd server {} (rev {}{})", + crate::VERSION, + crate::REV, + crate::DIRTY + ); + version_reported = true; + } + let ports = match refresh::get_entries().await { Ok(ports) => ports, Err(e) => { From e1768a043368e6d564150adbffb0bcfa22ad43df Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 10:15:42 -0700 Subject: [PATCH 35/85] Honor the configured description for a port, if any Get rid of that dumb warning, can't believe I forgot to wire this for so long. --- src/client/ui.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 923d731..24a4def 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -571,7 +571,7 @@ impl UI { // Grab the selected port let selected_port = self.get_selected_port(); - for port_desc in p.into_iter() { + for mut port_desc in p.into_iter() { leftover_ports.remove(&port_desc.port); if let Some(listener) = self.ports.get_mut(&port_desc.port) { @@ -579,6 +579,8 @@ impl UI { } else { let config = self.config.get(port_desc.port); info!("Port config {port_desc:?} -> {config:?}"); + port_desc.desc = + config.description.unwrap_or(port_desc.desc); self.ports.insert( port_desc.port, From 6736cdd431a7eb23f261d9ddc2051395e7b133aa Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 10:26:36 -0700 Subject: [PATCH 36/85] Additional tests for configuration --- src/client/config.rs | 5 ++++ src/client/ui.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/client/config.rs b/src/client/config.rs index c7f0bea..3d57e73 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -20,6 +20,11 @@ impl ServerConfig { ServerConfig { auto: true, ports: HashMap::new() } } + #[cfg(test)] + pub fn insert(&mut self, port: u16, config: PortConfig) { + self.ports.insert(port, config); + } + pub fn contains_key(&self, port: u16) -> bool { self.ports.contains_key(&port) } diff --git a/src/client/ui.rs b/src/client/ui.rs index 24a4def..50daac3 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -674,6 +674,8 @@ fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect { #[cfg(test)] mod tests { + use crate::client::config::PortConfig; + use super::*; use assert_matches::assert_matches; @@ -938,4 +940,56 @@ mod tests { assert_eq!(centered.width, 10); assert_eq!(centered.height, 10); } + + #[test] + fn port_config_description_respected() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8080, + PortConfig { + enabled: true, + description: Some("override".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + // There are ports... + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "my-service".to_string(), + }]))); + + let description = ui.ports.get(&8080).unwrap().desc.as_ref().unwrap(); + assert_eq!(description.desc, "override"); + + drop(sender); + } + + #[test] + fn port_config_enabled_respected() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8080, + PortConfig { + enabled: false, + description: Some("override".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + // There are ports... + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "my-service".to_string(), + }]))); + + let state = ui.ports.get(&8080).unwrap().state(); + assert_eq!(state, State::Disabled); + + drop(sender); + } } From 8f12945d83fd9a5a184d5a4c5131fca9d95c986c Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 14:26:41 -0700 Subject: [PATCH 37/85] Fix up port state interactions in the face of configuration --- src/client/config.rs | 12 +-- src/client/mod.rs | 7 +- src/client/ui.rs | 195 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 173 insertions(+), 41 deletions(-) diff --git a/src/client/config.rs b/src/client/config.rs index 3d57e73..0bd5cb1 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Result}; +use std::collections::hash_map; use std::collections::HashMap; use toml::Value; @@ -25,6 +26,10 @@ impl ServerConfig { self.ports.insert(port, config); } + pub fn iter(&self) -> hash_map::Iter { + self.ports.iter() + } + pub fn contains_key(&self, port: u16) -> bool { self.ports.contains_key(&port) } @@ -69,7 +74,7 @@ pub fn load_config() -> Result { }, }; - Ok(parse_config(&contents.parse::()?)?) + parse_config(&contents.parse::()?) } fn default() -> Config { @@ -84,10 +89,7 @@ fn parse_config(value: &Value) -> Result { Some(Value::Boolean(v)) => *v, Some(v) => bail!("expected a true or false, got {:?}", v), }; - Config { - auto, - servers: get_servers(&table, auto)?, - } + Config { auto, servers: get_servers(table, auto)? } }), _ => bail!("top level must be a table"), } diff --git a/src/client/mod.rs b/src/client/mod.rs index 94cb208..e3536b7 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -432,8 +432,13 @@ pub async fn run_client(remote: &str, sudo: bool, log_filter: &str) { _ = log::set_boxed_logger(ui::Logger::new(event_sender.clone())); log::set_max_level(LevelFilter::Info); + let server = if let Some((_user, server)) = remote.split_once("@") { + server + } else { + remote + }; let config = match config::load_config() { - Ok(config) => config.get(remote), + Ok(config) => config.get(server), Err(e) => { eprintln!("Error loading configuration: {:?}", e); return; diff --git a/src/client/ui.rs b/src/client/ui.rs index 50daac3..ccf772f 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -1,4 +1,7 @@ -use super::{client_listen, config::ServerConfig}; +use super::{ + client_listen, + config::{PortConfig, ServerConfig}, +}; use crate::message::PortDesc; use anyhow::Result; use copypasta::{ClipboardContext, ClipboardProvider}; @@ -75,6 +78,7 @@ pub enum State { Enabled, Broken, Disabled, + Configured, } impl State { @@ -86,35 +90,44 @@ impl State { #[derive(Debug)] struct Listener { state: std::sync::Arc>, + config: Option, stop: Option>, desc: Option, } impl Listener { - pub fn from_desc( - socks_port: Option, - desc: PortDesc, - enabled: bool, - ) -> Listener { + pub fn from_desc(socks_port: Option, desc: PortDesc) -> Listener { let mut listener = Listener { - state: if enabled { - State::Enabled.boxed() - } else { - State::Disabled.boxed() - }, + state: State::Enabled.boxed(), + config: None, stop: None, desc: Some(desc), }; - if enabled { - listener.start(socks_port); - } + listener.start(socks_port); listener } + pub fn from_config(config: PortConfig) -> Self { + Listener { + state: State::Configured.boxed(), + config: Some(config), + stop: None, + desc: None, + } + } + pub fn enabled(&self) -> bool { self.state() == State::Enabled } + pub fn description(&self) -> &str { + self.config + .as_ref() + .and_then(|c| c.description.as_deref()) + .or_else(|| self.desc.as_ref().map(|d| d.desc.as_str())) + .unwrap_or("") + } + fn state(&self) -> State { *self.state.lock().unwrap() } @@ -130,6 +143,16 @@ impl Listener { } pub fn connect(&mut self, socks_port: Option, desc: PortDesc) { + // If we're just sitting idle and the port comes in from the remote + // server then we should become enabled. Otherwise we should become + // real, but disabled. + if self.state() == State::Configured { + if self.config.as_ref().unwrap().enabled { + self.state = State::Enabled.boxed(); + } else { + self.state = State::Disabled.boxed(); + } + } self.desc = Some(desc); self.start(socks_port); } @@ -137,6 +160,13 @@ impl Listener { pub fn disconnect(&mut self) { self.desc = None; self.stop = None; + + // When we get disconnected, but we're present in the configuration, + // we go back to being merely 'Configured'. If the port shows up + // again, our auto-enable behavior will depend on the configuration. + if self.config.is_some() { + self.state = State::Configured.boxed(); + } } pub fn start(&mut self, socks_port: Option) { @@ -185,9 +215,14 @@ pub struct UI { impl UI { pub fn new(events: mpsc::Receiver, config: ServerConfig) -> UI { + let mut ports = HashMap::new(); + for (port, config) in config.iter() { + ports.insert(*port, Listener::from_config(config.clone())); + } + UI { events, - ports: HashMap::new(), + ports, socks_port: None, running: true, show_logs: false, @@ -288,13 +323,15 @@ impl UI { let (symbol, style) = match listener.state() { State::Enabled => (" ✓ ", enabled_port_style), State::Broken => (" ✗ ", broken_port_style), - State::Disabled => ("", disabled_port_style), + State::Disabled | State::Configured => { + ("", disabled_port_style) + } }; rows.push( Row::new(vec![ symbol, &*port_strings[index], - listener.desc.as_ref().map(|pd| &*pd.desc).unwrap_or(""), + listener.description(), ]) .style(style), ); @@ -390,11 +427,7 @@ impl UI { } fn enable_disable_port(&mut self, port: u16) { - let state = self.ports.get(&port).map(Listener::state); - if state == Some(State::Broken) { - // try turning it off and on again, it will at least get logs visible - self.ports.remove(&port); - } else if let Some(listener) = self.ports.get_mut(&port) { + if let Some(listener) = self.ports.get_mut(&port) { listener.set_enabled(self.socks_port, !listener.enabled()); } } @@ -571,24 +604,16 @@ impl UI { // Grab the selected port let selected_port = self.get_selected_port(); - for mut port_desc in p.into_iter() { + for port_desc in p.into_iter() { leftover_ports.remove(&port_desc.port); if let Some(listener) = self.ports.get_mut(&port_desc.port) { listener.connect(self.socks_port, port_desc); } else { - let config = self.config.get(port_desc.port); - info!("Port config {port_desc:?} -> {config:?}"); - port_desc.desc = - config.description.unwrap_or(port_desc.desc); - + assert!(!self.config.contains_key(port_desc.port)); self.ports.insert( port_desc.port, - Listener::from_desc( - self.socks_port, - port_desc, - config.enabled, - ), + Listener::from_desc(self.socks_port, port_desc), ); } } @@ -961,8 +986,8 @@ mod tests { desc: "my-service".to_string(), }]))); - let description = ui.ports.get(&8080).unwrap().desc.as_ref().unwrap(); - assert_eq!(description.desc, "override"); + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.description(), "override"); drop(sender); } @@ -992,4 +1017,104 @@ mod tests { drop(sender); } + + #[test] + fn port_config_missing_but_still_there() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8080, + PortConfig { + enabled: false, + description: Some("override".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + // There are no ports... + ui.handle_internal_event(Some(UIEvent::Ports(vec![]))); + + // But there should still be ports, man. + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Configured); + assert_eq!(listener.description(), "override"); + + drop(sender); + } + + #[test] + fn port_config_state_interactions() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert(8080, PortConfig { enabled: false, description: None }); + config.insert(8081, PortConfig { enabled: true, description: None }); + + let mut ui = UI::new(receiver, config); + + // No ports have been received, make sure everything's "configured" + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Configured); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + + // 8080 shows up.... not configured as enabled so it becomes "disabled" + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "python3".to_string(), + }]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + + // 8081 shows up.... configured as enabled so it becomes "enabled" + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { port: 8080, desc: "python3".to_string() }, + PortDesc { port: 8081, desc: "python3".to_string() }, + ]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + + // 8082 shows up.... it should be enabled by default! + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { port: 8080, desc: "python3".to_string() }, + PortDesc { port: 8081, desc: "python3".to_string() }, + PortDesc { port: 8082, desc: "python3".to_string() }, + ]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); + assert_eq!(ui.ports.get(&8082).unwrap().state(), State::Enabled); + + // 8081 goes away.... back to configured. + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { port: 8080, desc: "python3".to_string() }, + PortDesc { port: 8082, desc: "python3".to_string() }, + ]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + assert_eq!(ui.ports.get(&8082).unwrap().state(), State::Enabled); + + // All gone, state resets itself. + ui.handle_internal_event(Some(UIEvent::Ports(vec![]))); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Configured); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + assert!(!ui.ports.contains_key(&8082)); + + drop(sender); + } + + #[test] + fn port_defaults_respected() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "python3".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Enabled); + assert_eq!(listener.description(), "python3"); + + drop(sender); + } } From 0ad0fb1a5698a2eba74639d4df6cb33d8c00615a Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 18:21:47 -0700 Subject: [PATCH 38/85] That was completely wrong Look the whole point of the ports in the config is to have them enabled *even if the server doesn't show them to you.* The other behavior was just completely wrong in that respect. --- src/client/ui.rs | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index ccf772f..d49f927 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -78,7 +78,6 @@ pub enum State { Enabled, Broken, Disabled, - Configured, } impl State { @@ -109,7 +108,11 @@ impl Listener { pub fn from_config(config: PortConfig) -> Self { Listener { - state: State::Configured.boxed(), + state: if config.enabled { + State::Enabled.boxed() + } else { + State::Disabled.boxed() + }, config: Some(config), stop: None, desc: None, @@ -146,13 +149,6 @@ impl Listener { // If we're just sitting idle and the port comes in from the remote // server then we should become enabled. Otherwise we should become // real, but disabled. - if self.state() == State::Configured { - if self.config.as_ref().unwrap().enabled { - self.state = State::Enabled.boxed(); - } else { - self.state = State::Disabled.boxed(); - } - } self.desc = Some(desc); self.start(socks_port); } @@ -160,13 +156,6 @@ impl Listener { pub fn disconnect(&mut self) { self.desc = None; self.stop = None; - - // When we get disconnected, but we're present in the configuration, - // we go back to being merely 'Configured'. If the port shows up - // again, our auto-enable behavior will depend on the configuration. - if self.config.is_some() { - self.state = State::Configured.boxed(); - } } pub fn start(&mut self, socks_port: Option) { @@ -323,9 +312,7 @@ impl UI { let (symbol, style) = match listener.state() { State::Enabled => (" ✓ ", enabled_port_style), State::Broken => (" ✗ ", broken_port_style), - State::Disabled | State::Configured => { - ("", disabled_port_style) - } + State::Disabled => ("", disabled_port_style), }; rows.push( Row::new(vec![ @@ -1037,7 +1024,7 @@ mod tests { // But there should still be ports, man. let listener = ui.ports.get(&8080).unwrap(); - assert_eq!(listener.state(), State::Configured); + assert_eq!(listener.state(), State::Disabled); assert_eq!(listener.description(), "override"); drop(sender); @@ -1052,17 +1039,17 @@ mod tests { let mut ui = UI::new(receiver, config); - // No ports have been received, make sure everything's "configured" - assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Configured); - assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + // No ports have been received, make sure everything's in its default state. + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); - // 8080 shows up.... not configured as enabled so it becomes "disabled" + // 8080 shows up.... doesn't affect anything. ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { port: 8080, desc: "python3".to_string(), }]))); assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); - assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); // 8081 shows up.... configured as enabled so it becomes "enabled" ui.handle_internal_event(Some(UIEvent::Ports(vec![ @@ -1082,19 +1069,19 @@ mod tests { assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); assert_eq!(ui.ports.get(&8082).unwrap().state(), State::Enabled); - // 8081 goes away.... back to configured. + // 8081 goes away.... ui.handle_internal_event(Some(UIEvent::Ports(vec![ PortDesc { port: 8080, desc: "python3".to_string() }, PortDesc { port: 8082, desc: "python3".to_string() }, ]))); assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); - assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); assert_eq!(ui.ports.get(&8082).unwrap().state(), State::Enabled); // All gone, state resets itself. ui.handle_internal_event(Some(UIEvent::Ports(vec![]))); - assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Configured); - assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Configured); + assert_eq!(ui.ports.get(&8080).unwrap().state(), State::Disabled); + assert_eq!(ui.ports.get(&8081).unwrap().state(), State::Enabled); assert!(!ui.ports.contains_key(&8082)); drop(sender); From c6aa657b4cc5a1bd7b3bd52b2ad4ee7a7258db61 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 8 Aug 2024 21:13:14 -0700 Subject: [PATCH 39/85] Fix warning in windows --- src/reverse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reverse.rs b/src/reverse.rs index f717dd9..971d274 100644 --- a/src/reverse.rs +++ b/src/reverse.rs @@ -22,7 +22,7 @@ impl ReverseConnection { )) } - pub async fn send(&mut self, message: Message) -> Result<()> { + pub async fn send(&mut self, _message: Message) -> Result<()> { use anyhow::anyhow; Err(anyhow!( "Server-side operations are not supported on this platform" From e32f27494af440d2669341870c006c0f274affa5 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 9 Aug 2024 14:04:06 -0700 Subject: [PATCH 40/85] Tweak git status *again* Turns out that in order to go from clean to dirty you need to watch *everything*. Caching is hard, man. --- build.rs | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/build.rs b/build.rs index ed759f1..6d35333 100644 --- a/build.rs +++ b/build.rs @@ -84,23 +84,6 @@ fn emit_git_dirty() { let output = std::str::from_utf8(&output.stdout).expect("git did not output utf8"); - // If there *was* any output, parse it and tell cargo to re-run if any of - // these files changed. (Maybe they get reverted! Then the repo status - // will change.) - let mut split = output.split('\x00'); - while let Some(field) = split.next() { - if field.is_empty() { - continue; - } - let prefix = &field[0..3]; - println!("cargo::rerun-if-changed={}", &field[3..]); - let b = prefix.as_bytes(); - if b[0] == b'R' || b[1] == b'R' || b[0] == b'C' || b[1] == b'C' { - if let Some(additional) = split.next() { - println!("cargo::rerun-if-changed={additional}"); - } - } - } // Emit the repository status. let dirty = if output.trim().is_empty() { "" @@ -108,6 +91,33 @@ fn emit_git_dirty() { " *dirty*" }; println!("cargo::rustc-env=REPO_DIRTY={dirty}"); + + // NOW: The output here has to do with *all* of the files in the git + // respository. (Because if nothing was modified, but then *becomes* + // modified, we need to rerun the script to notice the dirty bit.) + // `git-ls-files` is the way to do that. + let output = std::process::Command::new("git") + .arg("ls-files") + .arg("-z") + .arg("--cached") + .arg("--deleted") + .arg("--modified") + .arg("--others") + .arg("--exclude-standard") + .output() + .expect("could not spawn `git` to get repository status"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf-8"); + eprintln!("`git ls-files` failed, stderr: {stderr}"); + panic!("`git ls-files` failed"); + } + let output = + std::str::from_utf8(&output.stdout).expect("git did not output utf8"); + + for fname in output.split_terminator("\0") { + println!("cargo::rerun-if-changed={fname}"); + } } fn main() { From cc004df6e8fc57064d7a23c976004d38b38ce789 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 06:42:57 -0700 Subject: [PATCH 41/85] Clippy --- src/client/ui.rs | 11 +++++------ src/message.rs | 3 +-- src/server/mod.rs | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index d49f927..d8a3751 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -476,15 +476,14 @@ impl UI { ev: Option>, ) { match ev { - Some(Ok(Event::Key(ev))) => match ev { + Some(Ok(Event::Key( KeyEvent { code: KeyCode::Esc, .. } | KeyEvent { code: KeyCode::Char('q'), .. } | KeyEvent { code: KeyCode::Char('?'), .. } - | KeyEvent { code: KeyCode::Char('h'), .. } => { - self.show_help = false; - } - _ => (), - }, + | KeyEvent { code: KeyCode::Char('h'), .. }, + ))) => { + self.show_help = false; + } Some(Ok(_)) => (), // Don't care about this event... Some(Err(_)) => (), // Hmmmmmm.....? None => (), // ....no events? what? diff --git a/src/message.rs b/src/message.rs index 6b77615..f347727 100644 --- a/src/message.rs +++ b/src/message.rs @@ -99,8 +99,7 @@ impl Message { result.put_u16(port.port); // Port descriptions can be long, let's make sure they're not. - let sliced = - slice_up_to(&port.desc, u16::max_value().into()); + let sliced = slice_up_to(&port.desc, u16::MAX.into()); put_string(result, sliced); } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 804d877..42b8799 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -114,7 +114,7 @@ mod tests { use tokio::io::{AsyncReadExt, DuplexStream}; async fn sync(client_read: &mut DuplexStream) { - print!("[client] Waiting for server sync...\n"); + println!("[client] Waiting for server sync..."); for _ in 0..8 { let b = client_read .read_u8() @@ -124,7 +124,7 @@ mod tests { } let mut reader = MessageReader::new(client_read); - print!("[client] Reading first message...\n"); + println!("[client] Reading first message..."); let msg = reader.read().await.expect("Error reading first message"); assert_matches!(msg, Message::Hello(0, 2, _)); } From de06612eb1d220a354ce6914f559d3de0e7391eb Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 06:54:26 -0700 Subject: [PATCH 42/85] Respect server-wide auto setting --- src/client/config.rs | 16 +++++++++------- src/client/ui.rs | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/client/config.rs b/src/client/config.rs index 0bd5cb1..f08220c 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -21,11 +21,20 @@ impl ServerConfig { ServerConfig { auto: true, ports: HashMap::new() } } + #[cfg(test)] + pub fn set_auto(&mut self, auto: bool) { + self.auto = auto; + } + #[cfg(test)] pub fn insert(&mut self, port: u16, config: PortConfig) { self.ports.insert(port, config); } + pub fn auto(&self) -> bool { + self.auto + } + pub fn iter(&self) -> hash_map::Iter { self.ports.iter() } @@ -33,13 +42,6 @@ impl ServerConfig { pub fn contains_key(&self, port: u16) -> bool { self.ports.contains_key(&port) } - - pub fn get(&self, port: u16) -> PortConfig { - match self.ports.get(&port) { - None => PortConfig { enabled: self.auto, description: None }, - Some(c) => c.clone(), - } - } } #[derive(Debug)] diff --git a/src/client/ui.rs b/src/client/ui.rs index d8a3751..7fb0968 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -95,9 +95,17 @@ struct Listener { } impl Listener { - pub fn from_desc(socks_port: Option, desc: PortDesc) -> Listener { + pub fn from_desc( + socks_port: Option, + desc: PortDesc, + enabled: bool, + ) -> Listener { let mut listener = Listener { - state: State::Enabled.boxed(), + state: if enabled { + State::Enabled.boxed() + } else { + State::Disabled.boxed() + }, config: None, stop: None, desc: Some(desc), @@ -599,7 +607,11 @@ impl UI { assert!(!self.config.contains_key(port_desc.port)); self.ports.insert( port_desc.port, - Listener::from_desc(self.socks_port, port_desc), + Listener::from_desc( + self.socks_port, + port_desc, + self.config.auto(), + ), ); } } @@ -1103,4 +1115,24 @@ mod tests { drop(sender); } + + #[test] + fn port_default_disabled_respected() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.set_auto(false); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "python3".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + assert_eq!(listener.description(), "python3"); + + drop(sender); + } } From b86a09131bd4bf60d939a40e3adb317a613cd7ce Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 06:58:22 -0700 Subject: [PATCH 43/85] Don't enable anonymous ports by default --- src/client/ui.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 7fb0968..8ee3691 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -605,12 +605,21 @@ impl UI { listener.connect(self.socks_port, port_desc); } else { assert!(!self.config.contains_key(port_desc.port)); + + // The server can send us these ports it knows nothing about. + // These might be dangerous to enable by default, so don't. + let enabled = if port_desc.desc.is_empty() { + false + } else { + self.config.auto() + }; + self.ports.insert( port_desc.port, Listener::from_desc( self.socks_port, port_desc, - self.config.auto(), + enabled, ), ); } @@ -1135,4 +1144,21 @@ mod tests { drop(sender); } + + #[test] + fn empty_port_desc_disabled_by_default() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + drop(sender); + } } From bb8c87bad903b027269a9f88425b5902b5985c90 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 07:02:46 -0700 Subject: [PATCH 44/85] Anonymous ports are described with a fixed string --- src/client/ui.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 8ee3691..f5cdbdc 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -132,11 +132,22 @@ impl Listener { } pub fn description(&self) -> &str { - self.config - .as_ref() - .and_then(|c| c.description.as_deref()) - .or_else(|| self.desc.as_ref().map(|d| d.desc.as_str())) - .unwrap_or("") + if let Some(config) = self.config.as_ref() { + if let Some(description) = config.description.as_deref() { + return description; + } + } + + if let Some(port) = self.desc.as_ref() { + let desc = port.desc.as_str(); + return if desc.is_empty() { + "" + } else { + desc + }; + } + + "" } fn state(&self) -> State { @@ -1158,6 +1169,7 @@ mod tests { let listener = ui.ports.get(&8080).unwrap(); assert_eq!(listener.state(), State::Disabled); + assert_eq!(listener.description(), ""); drop(sender); } From 69b9bc9824767b4fc525d41444f991a42d23f3cd Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 07:29:54 -0700 Subject: [PATCH 45/85] Sometimes you can't get a clipboard, don't fail This should fix CI as well, but there are possibly folks out there that don't have a functioning clipboard and still want to run. --- src/client/ui.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index f5cdbdc..4e1b42a 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -13,7 +13,7 @@ use crossterm::{ EnterAlternateScreen, LeaveAlternateScreen, }, }; -use log::{error, info, Level, Metadata, Record}; +use log::{error, info, warn, Level, Metadata, Record}; use std::collections::vec_deque::VecDeque; use std::collections::{HashMap, HashSet}; use std::io::stdout; @@ -218,7 +218,7 @@ pub struct UI { show_help: bool, alternate_screen: bool, raw_mode: bool, - clipboard: ClipboardContext, + clipboard: Option, } impl UI { @@ -228,6 +228,8 @@ impl UI { ports.insert(*port, Listener::from_config(config.clone())); } + let clipboard = ClipboardContext::new().ok(); + UI { events, ports, @@ -240,8 +242,7 @@ impl UI { config, alternate_screen: false, raw_mode: false, - clipboard: ClipboardContext::new() - .expect("Unable to initialize clipboard context"), + clipboard, } } @@ -672,10 +673,14 @@ impl UI { } Some(UIEvent::SetClipboard(contents)) => { let length = contents.len(); - if let Err(e) = self.clipboard.set_contents(contents) { - error!("Error setting clipboard contents: {e:#}"); + if let Some(clipboard) = self.clipboard.as_mut() { + if let Err(e) = clipboard.set_contents(contents) { + error!("Error setting clipboard contents: {e:#}"); + } else { + info!("Received clipboard contents ({length} bytes)"); + } } else { - info!("Received clipboard contents ({length} bytes)"); + warn!("No clipboard available, contents discarded"); } } None => { From a4745c92e258ee1ed7b4b6a737f24a4dfb05aece Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 07:44:06 -0700 Subject: [PATCH 46/85] Anonymous ports This is the other way to allow ports to work when the processes themselves cannot be enumerated: just report the port with an empty description. We need to do some work to make sure this is safe for the client; see comments. --- src/client/mod.rs | 1 + src/client/ui.rs | 26 ++++++++++++++++++++++++++ src/server/mod.rs | 11 ++++++++++- src/server/refresh.rs | 4 ++-- src/server/refresh/procfs.rs | 15 ++++++++++++--- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index e3536b7..90ec7fb 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -334,6 +334,7 @@ async fn spawn_ssh( cmd.arg("sudo"); } cmd.arg(format!("FWD_LOG={log_filter}")) + .arg("FWD_SEND_ANONYMOUS=1") .arg("fwd") .arg("--server"); diff --git a/src/client/ui.rs b/src/client/ui.rs index 4e1b42a..4ba2b04 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -1178,4 +1178,30 @@ mod tests { drop(sender); } + + #[test] + fn empty_port_desc_disabled_on_refresh() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + // Just do it again, make sure we haven't broken the refresh path. + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "".to_string(), + }]))); + + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + drop(sender); + } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 42b8799..101cbc4 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -24,6 +24,15 @@ async fn server_loop( reader: &mut MessageReader, writer: &mut mpsc::Sender, ) -> Result<()> { + // NOTE: The client needs to opt in to getting anonymous ports because it + // does not feel safe to automatically enable port forwarding by default + // for random system ports. The way we keep it from being unsafe is that + // the client leaves anonymous ports disabled by default. Older clients + // did not do this, and so we cannot send older clients anonymous ports. + let send_anonymous = std::env::var("FWD_SEND_ANONYMOUS") + .map(|v| v == "1") + .unwrap_or(false); + // The first message we send must be an announcement. writer.send(Message::Hello(0, 2, vec![])).await?; let mut version_reported = false; @@ -46,7 +55,7 @@ async fn server_loop( version_reported = true; } - let ports = match refresh::get_entries().await { + let ports = match refresh::get_entries(send_anonymous).await { Ok(ports) => ports, Err(e) => { error!("Error scanning: {:?}", e); diff --git a/src/server/refresh.rs b/src/server/refresh.rs index fcb47e3..fb1a6b7 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -12,7 +12,7 @@ mod procfs; #[cfg(unix)] mod docker; -pub async fn get_entries() -> Result> { +pub async fn get_entries(_send_anonymous: bool) -> Result> { #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] let mut attempts = 0; @@ -35,7 +35,7 @@ pub async fn get_entries() -> Result> { #[cfg(target_os = "linux")] { attempts += 1; - match procfs::get_entries() { + match procfs::get_entries(_send_anonymous) { Ok(m) => { for (p, d) in m { result.entry(p).or_insert(d); diff --git a/src/server/refresh/procfs.rs b/src/server/refresh/procfs.rs index aa1a79f..9749dde 100644 --- a/src/server/refresh/procfs.rs +++ b/src/server/refresh/procfs.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::message::PortDesc; -pub fn get_entries() -> Result> { +pub fn get_entries(send_anonymous: bool) -> Result> { let all_procs = procfs::process::all_processes()?; // build up a map between socket inodes and process stat info. Ignore any @@ -38,12 +38,21 @@ pub fn get_entries() -> Result> { || tcp_entry.local_address.ip().is_unspecified()) && !h.contains_key(&tcp_entry.local_address.port()) { - if let Some(cmd) = map.get(&tcp_entry.inode) { + // If the process is not one that we can identify, then we return + // the port but leave the description empty so that it can be + // identified by the client as "anonymous". + let desc = if let Some(cmd) = map.get(&tcp_entry.inode) { + cmd.clone() + } else { + String::new() + }; + + if send_anonymous || !desc.is_empty() { h.insert( tcp_entry.local_address.port(), PortDesc { port: tcp_entry.local_address.port(), - desc: cmd.clone(), + desc, }, ); } From a9bbd29f9f76f8960b15b62e577444f0b05feb4d Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 07:59:46 -0700 Subject: [PATCH 47/85] Fix enable/disable state changes Enabled -> Broken -> Disabled -> Enabled etc --- src/client/ui.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 4ba2b04..15ea41c 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -127,6 +127,11 @@ impl Listener { } } + #[cfg(test)] + pub fn state_ref(&self) -> std::sync::Arc> { + self.state.clone() + } + pub fn enabled(&self) -> bool { self.state() == State::Enabled } @@ -154,8 +159,8 @@ impl Listener { *self.state.lock().unwrap() } - pub fn set_enabled(&mut self, socks_port: Option, enabled: bool) { - if enabled { + pub fn toggle_enabled(&mut self, socks_port: Option) { + if self.state() == State::Disabled { self.state = State::Enabled.boxed(); self.start(socks_port); } else { @@ -435,7 +440,7 @@ impl UI { fn enable_disable_port(&mut self, port: u16) { if let Some(listener) = self.ports.get_mut(&port) { - listener.set_enabled(self.socks_port, !listener.enabled()); + listener.toggle_enabled(self.socks_port); } } @@ -1204,4 +1209,46 @@ mod tests { drop(sender); } + + #[test] + fn state_toggle_enable_disable() { + let (sender, receiver) = mpsc::channel(64); + let config = ServerConfig::default(); + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc { + port: 8080, + desc: "rando".to_string(), + }]))); + + let listener = ui.ports.get_mut(&8080).unwrap(); + assert_eq!(listener.state(), State::Enabled); + + // Enabled -> Disabled + ui.enable_disable_port(8080); // FLIP! + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + // Disabled -> Enabled + ui.enable_disable_port(8080); // FLIP! + let listener = ui.ports.get(&8080).unwrap(); + assert_eq!(listener.state(), State::Enabled); + + { + // Oh no it broke! + let state = listener.state_ref(); + let mut sg = state.lock().unwrap(); + *sg = State::Broken; + } + + let listener = ui.ports.get_mut(&8080).unwrap(); + assert_eq!(listener.state(), State::Broken); + + // Broken -> Disabled + ui.enable_disable_port(8080); + let listener = ui.ports.get_mut(&8080).unwrap(); + assert_eq!(listener.state(), State::Disabled); + + drop(sender); + } } From 9ad55c903fd6258d48bf66fd2384e11aa60d7f2c Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 08:13:45 -0700 Subject: [PATCH 48/85] Speculative changes to github workflows --- .github/workflows/ci.yaml | 2 +- .github/workflows/release.yaml | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eb047ea..8e19021 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build run: cargo build --verbose diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cbda082..5e85acc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -46,8 +46,6 @@ jobs: TARGET_DIR: ./target # Emit backtraces on panics. RUST_BACKTRACE: 1 - # Build static releases with PCRE2. - PCRE2_SYS_STATIC: 1 strategy: matrix: @@ -55,32 +53,27 @@ jobs: include: - build: linux os: ubuntu-22.04 - rust: nightly target: x86_64-unknown-linux-musl - build: macos os: macos-12 - rust: nightly target: x86_64-apple-darwin - build: arm-macos os: macos-12 - rust: nightly target: aarch64-apple-darwin - build: windows os: windows-2022 - rust: nightly target: x86_64-pc-windows-msvc steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@master + uses: dtolnay/rust-toolchain@stable with: - toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - name: Use Cross From 03de4a4661b25d2a87870963974de279222b9802 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 10 Aug 2024 09:00:20 -0700 Subject: [PATCH 49/85] Yet another tweak to git process for cargo publish --- build.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/build.rs b/build.rs index 6d35333..b0c87c2 100644 --- a/build.rs +++ b/build.rs @@ -21,14 +21,36 @@ fn file_contents>(path: P) -> String { std::fs::read_to_string(path).expect("Unable to read file") } +fn git_rel>(path: P) -> PathBuf { + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg("--show-toplevel") + .output() + .expect("Error launching git rev-parse"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .expect("git failed and stderr was not utf8"); + eprintln!("`git rev-parse --show-toplevel` failed, stderr: {stderr}"); + panic!("`git rev-parse --show-toplevel` failed"); + } + + let mut root = PathBuf::from( + std::str::from_utf8(&output.stdout) + .expect("Output was not utf-8") + .trim(), + ); + root.push(path); + root +} + /// Emit the current git commit. fn emit_git_commit() { // Fetch the current commit from the head. We do it this way instead of // asking `git rev-parse` to do it for us because we want to reliably // tell cargo which files it should monitor for changes. - let head = file_contents("./.git/HEAD"); + let head = file_contents(git_rel(".git/HEAD")); let rev = if let Some(r) = head.strip_prefix("ref: ") { - let mut ref_path = PathBuf::from("./.git/"); + let mut ref_path = git_rel(".git/"); ref_path.push(r.trim()); file_contents(ref_path) } else { From 4647226ee76553f001cd153dcd4ca055be2131fd Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 09:11:21 -0700 Subject: [PATCH 50/85] Handle blank input a little more cleanly --- src/server/refresh/docker.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs index 1085e6d..a8a55fe 100644 --- a/src/server/refresh/docker.rs +++ b/src/server/refresh/docker.rs @@ -105,8 +105,14 @@ impl JsonValue { pub fn parse(blob: &[u8]) -> Result { Self::parse_impl(blob).with_context(|| { match std::str::from_utf8(blob) { - Ok(s) => format!("Failed to parse: {s}"), - Err(_) => format!("Failed to parse {blob:?}"), + Ok(s) => format!("Failed to parse {} bytes: '{}'", s.len(), s), + Err(_) => { + format!( + "Failed to parse {} bytes (not utf-8): {:?}", + blob.len(), + blob + ) + } } }) } @@ -295,10 +301,11 @@ impl JsonValue { } } - match stack.pop().expect("underflow somehow") { - Tok::Val(v) => Ok(v), - Tok::StartObject => bail!("unterminated object"), - Tok::StartArray => bail!("unterminated array"), + match stack.pop() { + Some(Tok::Val(v)) => Ok(v), + Some(Tok::StartObject) => bail!("unterminated object"), + Some(Tok::StartArray) => bail!("unterminated array"), + None => bail!("No JSON found in input"), } } @@ -501,6 +508,11 @@ mod test { } } + #[test] + pub fn json_decode_empty() { + assert!(JsonValue::parse(b" ").is_err()); + } + #[test] pub fn json_decode_docker() { use pretty_assertions::assert_eq; From 9b0a39fa905bad96b56a5da7f2c6edf49da7d348 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 09:14:22 -0700 Subject: [PATCH 51/85] Bump crate version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2e5481..a924a4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,7 +320,7 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "fwd" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index e7d8fc2..43098c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.9.0" +version = "0.9.1" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" From 77cbf1700f5492f449f459fa19a40f88e494e93f Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 09:41:22 -0700 Subject: [PATCH 52/85] Check for unterminated strings properly Also, public to enable fuzzing. This was the first catch! --- src/lib.rs | 2 +- src/server/mod.rs | 2 +- src/server/refresh.rs | 2 +- src/server/refresh/docker.rs | 10 ++++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6920aa0..d90a75e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ mod client; mod message; mod reverse; -mod server; +pub mod server; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const REV: &str = env!("REPO_REV"); diff --git a/src/server/mod.rs b/src/server/mod.rs index 101cbc4..db56ce2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -5,7 +5,7 @@ use log::{error, warn}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; use tokio::sync::mpsc; -mod refresh; +pub mod refresh; // We drive writes through an mpsc queue, because we not only handle requests // and responses from the client (refresh ports and the like) but also need diff --git a/src/server/refresh.rs b/src/server/refresh.rs index fb1a6b7..a1a3d05 100644 --- a/src/server/refresh.rs +++ b/src/server/refresh.rs @@ -10,7 +10,7 @@ use crate::message::PortDesc; mod procfs; #[cfg(unix)] -mod docker; +pub mod docker; pub async fn get_entries(_send_anonymous: bool) -> Result> { #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs index a8a55fe..fbce5c1 100644 --- a/src/server/refresh/docker.rs +++ b/src/server/refresh/docker.rs @@ -77,7 +77,7 @@ async fn list_containers() -> Result> { } #[derive(Debug, PartialEq)] -enum JsonValue { +pub enum JsonValue { Null, True, False, @@ -207,7 +207,7 @@ impl JsonValue { } i += 1; } - if i == blob.len() { + if i >= blob.len() { bail!("Unterminated string at {i}"); } assert_eq!(blob[i], b'"'); @@ -874,4 +874,10 @@ mod test { ]); assert_eq!(result, expected); } + + #[test] + pub fn json_decode_unterminated_string_with_escape() { + let input = b"\"\\"; + let _ = JsonValue::parse(input); + } } From e27b788e8f612ceaa2b22372d8d00c1fdc47770f Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 09:43:56 -0700 Subject: [PATCH 53/85] Fuzzing for the json decoder Hey it seems like it's working! --- fuzz/.gitignore | 4 + fuzz/Cargo.lock | 1559 ++++++++++++++++++++++++++++ fuzz/Cargo.toml | 21 + fuzz/fuzz_targets/fuzz_target_1.rs | 11 + 4 files changed, 1595 insertions(+) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/fuzz_target_1.rs diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..c6243e0 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,1559 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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 = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix 0.38.34", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.6", +] + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "futures-core", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "env_filter", + "log", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "fwd" +version = "0.9.1" +dependencies = [ + "anyhow", + "bytes", + "copypasta", + "crossterm", + "env_logger", + "home", + "indoc", + "log", + "open", + "procfs", + "rand", + "thiserror", + "tokio", + "tokio-stream", + "toml", + "tui", + "users", + "xdg", +] + +[[package]] +name = "fwd-fuzz" +version = "0.0.0" +dependencies = [ + "fwd", + "libfuzzer-sys", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[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 = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[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.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "flate2", + "hex", + "lazy_static", + "rustix 0.36.17", +] + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.36.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[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.206" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.206" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.2", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags 1.3.2", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wayland-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.34", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +dependencies = [ + "bitflags 2.6.0", + "rustix 0.38.34", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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", + "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_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.34", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..234203f --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fwd-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.fwd] +path = ".." + +[[bin]] +name = "fuzz_target_1" +path = "fuzz_targets/fuzz_target_1.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/fuzz_target_1.rs new file mode 100644 index 0000000..94bd001 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_target_1.rs @@ -0,0 +1,11 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +extern crate fwd; +use fwd::server::refresh::docker::JsonValue; + +fuzz_target!(|data: &[u8]| { + // fuzzed code goes here + let _ = JsonValue::parse(data); +}); From 665fccf75315d6ecb66804d5f73b14f61b31c484 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 10:07:42 -0700 Subject: [PATCH 54/85] Add trace logging to the docker refresh That way we can see what's going on with docker responses if they're weird. --- src/server/refresh/docker.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs index fbce5c1..b086f9d 100644 --- a/src/server/refresh/docker.rs +++ b/src/server/refresh/docker.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Context, Result}; +use log::trace; use std::collections::HashMap; use tokio::io::{ AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, @@ -26,6 +27,7 @@ Accept: */*\r\n\ // Check the HTTP response. let mut line = String::new(); stream.read_line(&mut line).await?; + trace!("[docker] {}", line.trim_end()); let parts: Vec<&str> = line.split(" ").collect(); if parts.len() < 2 || parts[1] != "200" { bail!("Error response from docker: {line}"); @@ -36,6 +38,7 @@ Accept: */*\r\n\ loop { line.clear(); stream.read_line(&mut line).await?; + trace!("[docker] {}", line.trim_end()); if line.trim().is_empty() { break; } @@ -49,6 +52,17 @@ Accept: */*\r\n\ let mut response_buffer = vec![0; content_length]; stream.read_exact(&mut response_buffer).await?; + if log::log_enabled!(log::Level::Trace) { + match std::str::from_utf8(&response_buffer) { + Ok(s) => trace!("[docker][{}b] {}", s.len(), s), + Err(_) => trace!( + "[docker][{}b, raw] {:?}", + response_buffer.len(), + &response_buffer + ), + } + } + // Done with the stream. Ok(response_buffer) } @@ -56,6 +70,7 @@ Accept: */*\r\n\ async fn list_containers() -> Result> { let host = std::env::var("DOCKER_HOST") .unwrap_or_else(|_| DEFAULT_DOCKER_HOST.to_string()); + trace!("[docker] Connecting to {host}"); match host { h if h.starts_with("unix://") => { let socket_path = &h[7..]; From 542127f72345e5e98afd13d6ff355ce1c15a5fcd Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 11:28:59 -0700 Subject: [PATCH 55/85] Handle transfer-encoding chunked in docker responses Yeah, OK, thanks HTTP. --- src/server/refresh/docker.rs | 145 +++++++++++++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 7 deletions(-) diff --git a/src/server/refresh/docker.rs b/src/server/refresh/docker.rs index b086f9d..06d4fff 100644 --- a/src/server/refresh/docker.rs +++ b/src/server/refresh/docker.rs @@ -20,6 +20,7 @@ Host: localhost\r\n\ User-Agent: fwd/1.0\r\n\ Accept: */*\r\n\ \r\n"; + let mut stream = tokio::io::BufStream::new(stream); stream.write_all(DOCKER_LIST_CONTAINERS).await?; stream.flush().await?; @@ -27,14 +28,15 @@ Accept: */*\r\n\ // Check the HTTP response. let mut line = String::new(); stream.read_line(&mut line).await?; - trace!("[docker] {}", line.trim_end()); + trace!("[docker] {}", &line.trim_end()); let parts: Vec<&str> = line.split(" ").collect(); if parts.len() < 2 || parts[1] != "200" { - bail!("Error response from docker: {line}"); + bail!("Error response from docker: {line:?}"); } - // Process the headers; all we really care about is content-length. - let mut content_length: usize = 0; + // Process the headers; all we really care about is content-length or content-encoding. + let mut content_length: Option = None; + let mut chunked = false; loop { line.clear(); stream.read_line(&mut line).await?; @@ -44,13 +46,55 @@ Accept: */*\r\n\ } line.make_ascii_lowercase(); if let Some(rest) = line.strip_prefix("content-length: ") { - content_length = rest.trim().parse()?; + content_length = Some(rest.trim().parse()?); + } + if let Some(rest) = line.strip_prefix("transfer-encoding: ") { + chunked = rest.trim() == "chunked"; } } // Read the JSON response. - let mut response_buffer = vec![0; content_length]; - stream.read_exact(&mut response_buffer).await?; + let mut response_buffer = vec![0; content_length.unwrap_or(0)]; + if content_length.is_some() { + stream.read_exact(&mut response_buffer).await?; + } else if chunked { + // Docker will send a chunked encoding if the response seems too big to do + // all at once. I don't know the heuristic it uses but we need to deal with + // it. Fortunately chunked encoding is not too bad? + loop { + line.clear(); + stream.read_line(&mut line).await?; + // This is the hex length of the thing. + let Some(chunk_length) = line.split(";").next() else { + bail!("Can't make sense of chunk length line: {line:?}"); + }; + let Ok(chunk_length) = + usize::from_str_radix(chunk_length.trim(), 16) + else { + bail!("Cannot interpret chunk length '{chunk_length}' as hex (Full line: {line:?})"); + }; + if chunk_length > 0 { + let old_length = response_buffer.len(); + let new_length = old_length + chunk_length; + response_buffer.resize(new_length, 0); + stream + .read_exact(&mut response_buffer[old_length..new_length]) + .await?; + } + + let mut eol: [u8; 2] = [0, 0]; + stream.read_exact(&mut eol).await?; + if eol[0] != b'\r' || eol[1] != b'\n' { + bail!("Mal-formed end-of-chunk marker from server"); + } + if chunk_length == 0 { + break; // All done. + } + } + } else { + trace!("Docker did not send a content_length, just reading to the end"); + stream.read_to_end(&mut response_buffer).await?; + } if log::log_enabled!(log::Level::Trace) { match std::str::from_utf8(&response_buffer) { @@ -895,4 +939,91 @@ mod test { let input = b"\"\\"; let _ = JsonValue::parse(input); } + + async fn accept_and_send_single_response( + listener: tokio::net::TcpListener, + response: &[u8], + ) { + println!("[server] Awaiting connection..."); + let (stream, _) = listener + .accept() + .await + .expect("Unable to accept connection"); + let mut stream = tokio::io::BufStream::new(stream); + + println!("[server] Reading request..."); + let mut line = String::new(); + loop { + line.clear(); + stream + .read_line(&mut line) + .await + .expect("Unable to read line in server"); + if line.trim().is_empty() { + break; + } + } + + println!("[server] Sending response..."); + stream + .write_all(response) + .await + .expect("Unable to write response"); + stream.flush().await.expect("Unable to flush"); + println!("[server] Done."); + } + + #[tokio::test] + pub async fn docker_chunked_transfer_encoding() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("Unable to create listener on localhost"); + let port = listener.local_addr().unwrap().port(); + + let mut set = tokio::task::JoinSet::new(); + set.spawn(async move { + const RESPONSE: &[u8] = b"\ +HTTP/1.1 200 OK\r\n\ +Transfer-Encoding: chunked\r\n\ +\r\n\ +4\r\nWiki\r\n7\r\npedia i\r\nB\r\nn \r\nchunks.\r\n0\r\n\r\n"; + + accept_and_send_single_response(listener, RESPONSE).await; + }); + + let addr = format!("127.0.0.1:{port}"); + let stream = tokio::net::TcpStream::connect(&addr) + .await + .expect("Unable to connect"); + let response = list_containers_with_connection(stream) + .await + .expect("Unable to get response"); + assert_eq!(&response, b"Wikipedia in \r\nchunks."); + } + + #[tokio::test] + pub async fn docker_with_no_content_length() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("Unable to create listener on localhost"); + let port = listener.local_addr().unwrap().port(); + + let mut set = tokio::task::JoinSet::new(); + set.spawn(async move { + const RESPONSE: &[u8] = b"\ +HTTP/1.1 200 OK\r\n\ +\r\n\ +[\"Booo this is some data\"]\r\n"; + accept_and_send_single_response(listener, RESPONSE).await; + }); + + let addr = format!("127.0.0.1:{port}"); + let stream = tokio::net::TcpStream::connect(&addr) + .await + .expect("Unable to connect"); + let response = list_containers_with_connection(stream) + .await + .expect("Unable to get response"); + assert_eq!(&response, b"[\"Booo this is some data\"]\r\n"); + } } From 43f6b75762826781df0baf3f49117f752b072092 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 11:44:02 -0700 Subject: [PATCH 56/85] Rename fuzz target to something more meaningful --- fuzz/Cargo.toml | 4 ++-- fuzz/fuzz_targets/{fuzz_target_1.rs => json_raw_input.rs} | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) rename fuzz/fuzz_targets/{fuzz_target_1.rs => json_raw_input.rs} (86%) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 234203f..0eeeb41 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -14,8 +14,8 @@ libfuzzer-sys = "0.4" path = ".." [[bin]] -name = "fuzz_target_1" -path = "fuzz_targets/fuzz_target_1.rs" +name = "json_raw_input" +path = "fuzz_targets/json_raw_input.rs" test = false doc = false bench = false diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/json_raw_input.rs similarity index 86% rename from fuzz/fuzz_targets/fuzz_target_1.rs rename to fuzz/fuzz_targets/json_raw_input.rs index 94bd001..3178e8b 100644 --- a/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/fuzz/fuzz_targets/json_raw_input.rs @@ -6,6 +6,5 @@ extern crate fwd; use fwd::server::refresh::docker::JsonValue; fuzz_target!(|data: &[u8]| { - // fuzzed code goes here let _ = JsonValue::parse(data); }); From 35dcf9397127cd6c57326a2f74c6666bfaa837bb Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 17:18:26 -0700 Subject: [PATCH 57/85] Add fuzzing based on serde_json This test ensures that we can parse anything that serde_json can produce, which *ought* to ensure reasonable coverage? --- fuzz/Cargo.lock | 40 +++++++++++ fuzz/Cargo.toml | 9 +++ fuzz/fuzz_targets/json_only_valid_serde.rs | 77 ++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 fuzz/fuzz_targets/json_only_valid_serde.rs diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index c6243e0..463a394 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -43,6 +43,9 @@ name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "autocfg" @@ -247,6 +250,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -335,8 +349,10 @@ dependencies = [ name = "fwd-fuzz" version = "0.0.0" dependencies = [ + "arbitrary", "fwd", "libfuzzer-sys", + "serde_json", ] [[package]] @@ -433,6 +449,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "jobserver" version = "0.1.32" @@ -817,6 +839,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -849,6 +877,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0eeeb41..bc252d7 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,7 +8,9 @@ edition = "2021" cargo-fuzz = true [dependencies] +arbitrary = { version = "1.3.2", features = ["derive"] } libfuzzer-sys = "0.4" +serde_json = "1.0.124" [dependencies.fwd] path = ".." @@ -19,3 +21,10 @@ path = "fuzz_targets/json_raw_input.rs" test = false doc = false bench = false + +[[bin]] +name = "json_only_valid_serde" +path = "fuzz_targets/json_only_valid_serde.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/json_only_valid_serde.rs b/fuzz/fuzz_targets/json_only_valid_serde.rs new file mode 100644 index 0000000..567642f --- /dev/null +++ b/fuzz/fuzz_targets/json_only_valid_serde.rs @@ -0,0 +1,77 @@ +#![no_main] + +use arbitrary::{Arbitrary, Error, Unstructured}; +use libfuzzer_sys::fuzz_target; +use std::collections::HashMap; + +extern crate fwd; +use fwd::server::refresh::docker::JsonValue; + +/// InputNumber is a JSON number, i.e., a finite 64-bit floating point value +/// that is not NaN. We need to define our own little wrapper here so that we +/// can convince Arbitrary to only make finite f64s. +/// +/// Ideally we would actually wrap serde_json::Number but there are rules +/// about mixing 3rd party traits with 3rd party types. +#[derive(Debug, PartialEq)] +struct InputNumber(f64); + +impl<'a> Arbitrary<'a> for InputNumber { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let value = f64::arbitrary(u)?; + if value.is_finite() { + Ok(InputNumber(value)) + } else { + Err(Error::IncorrectFormat) // REJECT + } + } + + #[inline] + fn size_hint(depth: usize) -> (usize, Option) { + f64::size_hint(depth) + } +} + +/// TestInput is basically serde_json::Value, except (a) it has a HashMap and +/// not serde_json's special `Map` structure, and (b) it has `InputNumber` +/// instead of `json_serde::Number` for reasons described above. +#[derive(Debug, PartialEq, Arbitrary)] +enum TestInput { + Null, + Bool(bool), + Number(InputNumber), + String(String), + Object(HashMap), + Array(Vec), +} + +fn convert(value: &TestInput) -> serde_json::Value { + match value { + TestInput::Null => serde_json::Value::Null, + TestInput::Bool(b) => serde_json::Value::Bool(*b), + TestInput::Number(n) => serde_json::Value::Number( + serde_json::Number::from_f64(n.0).expect("Unable to make an f64"), + ), + TestInput::String(s) => serde_json::Value::String(s.clone()), + TestInput::Object(o) => { + let mut out = serde_json::map::Map::new(); + for (k, v) in o.into_iter() { + out.insert(k.clone(), convert(v)); + } + serde_json::Value::Object(out) + } + TestInput::Array(v) => { + serde_json::Value::Array(v.into_iter().map(convert).collect()) + } + } +} + +fuzz_target!(|data: TestInput| { + // Convert the arbitrary TestInput into an arbitrary serde_json::Value, + // then use serde_json to write out arbitrary JSON. + let converted = convert(&data).to_string(); + + // Parse the JSON that serde_json produced. This fuzz test should ensure + // that we can parse anything that serde_json can produce. + let _ = JsonValue::parse(converted.as_bytes()); +}); From df0ca4ce319954e9d03daf294647024993ccc37b Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 17:37:34 -0700 Subject: [PATCH 58/85] Remove "users" crate, call libc directly This is all I actually needed anyways --- Cargo.lock | 12 +----------- Cargo.toml | 2 +- src/reverse/unix.rs | 3 ++- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a924a4b..18e0d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,7 @@ dependencies = [ "env_logger", "home", "indoc", + "libc", "log", "open", "pretty_assertions", @@ -341,7 +342,6 @@ dependencies = [ "tokio-stream", "toml", "tui", - "users", "xdg", ] @@ -1114,16 +1114,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "users" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" -dependencies = [ - "libc", - "log", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 43098c8..107266e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,4 +40,4 @@ tempdir = "0.3" procfs = "0.14.1" [target.'cfg(target_family="unix")'.dependencies] -users = "0.11" +libc = "0.2.155" diff --git a/src/reverse/unix.rs b/src/reverse/unix.rs index d3cc801..b85ed54 100644 --- a/src/reverse/unix.rs +++ b/src/reverse/unix.rs @@ -55,7 +55,8 @@ fn socket_directory() -> Result { Ok(path) => Ok(path), Err(_) => { let mut path = std::env::temp_dir(); - path.push(format!("fwd{}", users::get_current_uid())); + let uid = unsafe { libc::getuid() }; + path.push(format!("fwd{}", uid)); Ok(path) } } From a7202010d068ebf47eefd042d30465dd6184a613 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 17:43:17 -0700 Subject: [PATCH 59/85] Probably we should run tests as part of release? --- .github/workflows/release.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5e85acc..2241d1d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -84,6 +84,9 @@ jobs: echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV + - name: Run the tests + run: ${{ env.CARGO }} test --verbose --release ${{ env.TARGET_FLAGS }} + - name: Build release binary run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} From df914e68f21cf93edad4b051230d6ed97128c611 Mon Sep 17 00:00:00 2001 From: John Doty Date: Mon, 12 Aug 2024 17:57:36 -0700 Subject: [PATCH 60/85] I *think* I need something other than macos-12 for aarch64 https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2241d1d..dc2607c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -56,11 +56,11 @@ jobs: target: x86_64-unknown-linux-musl - build: macos - os: macos-12 + os: macos-latest target: x86_64-apple-darwin - build: arm-macos - os: macos-12 + os: macos-latest target: aarch64-apple-darwin - build: windows From 68f3c4fa4e604ac43e8ee1f23939da1db29143a8 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 07:23:59 -0700 Subject: [PATCH 61/85] Experimental updates to release workflow --- .github/workflows/release.yaml | 51 ++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dc2607c..4ef6b2f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,27 +13,43 @@ on: tags: - "v[0-9]+.[0-9]+.[0-9]+" +permissions: + contents: write + jobs: create_release: name: Create release - runs-on: ubuntu-22.04 - - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} + runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + + - name: Get the release version + if: env.VERSION == '' + run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + - name: Show the version + run: | + echo "version is: $VERSION" + + - name: Check that tag version and Cargo.toml version are the same + shell: bash + run: | + if ! grep -q "version = \"$VERSION\"" Cargo.toml; then + echo "version does not match Cargo.toml" >&2 + exit 1 + fi + - name: Create GitHub release - id: create_release - uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: true + run: gh release create $VERSION --draft --verify-tag --title $VERSION - release_assets: - name: Release assets + outputs: + version: ${{ env.VERSION }} + + build_release: + name: Build all the stuff needs: ['create_release'] # We need to know the upload URL runs-on: ${{ matrix.os }} # We run many different builds env: @@ -48,6 +64,7 @@ jobs: RUST_BACKTRACE: 1 strategy: + fail-fast: false matrix: build: ['linux', 'macos', 'arm-macos', 'windows'] include: @@ -114,11 +131,9 @@ jobs: fi - name: Upload release archive - uses: actions/upload-release-asset@v1.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: ${{ env.ASSET }} - asset_path: ${{ env.ASSET }} - asset_content_type: application/octet-stream + shell: bash + run: | + version="${{ needs.create_release.outputs.version }}" + gh release upload "$version" ${{ env.ASSET }} From 7e047626dfb47fdef6dea6e3f937cf895b110853 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 07:24:29 -0700 Subject: [PATCH 62/85] Bump to the next version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18e0d1f..f77556e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,7 +320,7 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "fwd" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index 107266e..97dfd81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fwd" -version = "0.9.1" +version = "0.9.2" edition = "2021" license = "MIT" description = "Automatically forward ports to a remote server over ssh" From b381f716926a6d499182e762ceac44161b10e0a6 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:44:58 -0700 Subject: [PATCH 63/85] Move from tui to ratatui Tui is no longer supported, ratatui is the new hotness. Fortunately there is very little difference between the two, except I've noticed a fun new bug in the help screen. (Maybe it's been there the whole time?) --- Cargo.lock | 617 +++++++++++++++++++++++++++++++---------------- Cargo.toml | 4 +- src/client/ui.rs | 38 ++- 3 files changed, 423 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f77556e..111ae68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -34,9 +52,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "assert_matches" @@ -46,15 +64,15 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -73,9 +91,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block" @@ -85,9 +103,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -97,17 +115,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "calloop" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "log", "polling", "rustix 0.38.34", @@ -117,9 +135,9 @@ dependencies = [ [[package]] name = "calloop-wayland-source" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", "rustix 0.38.34", @@ -134,14 +152,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] -name = "cc" -version = "1.0.83" +name = "castaway" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ - "libc", + "rustversion", ] +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" + [[package]] name = "cfg-if" version = "1.0.0" @@ -150,14 +174,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -170,6 +194,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -195,15 +233,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -216,16 +254,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.25.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", "mio", "parking_lot", + "rustix 0.38.34", "signal-hook", "signal-hook-mio", "winapi", @@ -267,6 +305,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -298,9 +342,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", @@ -314,9 +358,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "fwd" @@ -336,12 +380,12 @@ dependencies = [ "pretty_assertions", "procfs", "rand 0.8.5", + "ratatui", "tempdir", "thiserror", "tokio", "tokio-stream", "toml", - "tui", "xdg", ] @@ -357,9 +401,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -368,15 +412,31 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" @@ -392,18 +452,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -428,22 +488,47 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] [[package]] -name = "js-sys" -version = "0.3.65" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -456,9 +541,9 @@ checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -468,12 +553,12 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -490,9 +575,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -504,6 +589,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -515,9 +609,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -530,44 +624,35 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.3", - "libc", -] - [[package]] name = "objc" version = "0.2.7" @@ -599,18 +684,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" @@ -624,9 +709,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -634,17 +719,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -653,9 +744,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pkg-config" @@ -665,9 +756,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" -version = "3.7.2" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", @@ -675,14 +766,17 @@ dependencies = [ "pin-project-lite", "rustix 0.38.34", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -696,9 +790,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -720,18 +814,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -794,6 +888,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -805,11 +920,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] @@ -823,9 +938,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -847,13 +962,25 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -868,18 +995,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", @@ -898,9 +1025,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -909,9 +1036,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -927,17 +1054,17 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -958,9 +1085,9 @@ dependencies = [ [[package]] name = "smithay-clipboard" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", "smithay-client-toolkit", @@ -969,19 +1096,47 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", ] [[package]] name = "syn" -version = "2.0.39" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -1000,18 +1155,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -1020,27 +1175,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.34.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -1083,19 +1237,6 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags 1.3.2", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1104,15 +1245,32 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1122,19 +1280,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -1147,9 +1306,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1157,9 +1316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -1170,15 +1329,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wayland-backend" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" dependencies = [ "cc", "downcast-rs", @@ -1190,11 +1349,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "rustix 0.38.34", "wayland-backend", "wayland-scanner", @@ -1206,16 +1365,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" dependencies = [ "rustix 0.38.34", "wayland-client", @@ -1224,11 +1383,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.31.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1236,11 +1395,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.2.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1249,9 +1408,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2", "quick-xml", @@ -1260,9 +1419,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" dependencies = [ "dlib", "log", @@ -1294,11 +1453,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1340,7 +1499,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "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]] @@ -1375,18 +1543,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "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]] @@ -1403,9 +1571,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1421,9 +1589,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1439,15 +1607,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1463,9 +1631,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1481,9 +1649,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1499,9 +1667,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1517,9 +1685,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "x11-clipboard" @@ -1550,9 +1718,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xdg" @@ -1571,3 +1739,24 @@ name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 97dfd81..0426ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,18 +17,18 @@ bench = false anyhow = "1.0" bytes = "1" copypasta = "0.10.1" -crossterm = { version = "0.25", features = ["event-stream"] } +crossterm = { version = "0.28.1", features = ["event-stream"] } env_logger = { version = "0.11.5", default-features = false } home = "0.5.4" indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" rand = "0.8.5" +ratatui = "0.28.0" thiserror = "1.0" tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" toml = "0.5" -tui = "0.19" xdg = "2" [dev-dependencies] diff --git a/src/client/ui.rs b/src/client/ui.rs index 15ea41c..4a3d2df 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -14,15 +14,8 @@ use crossterm::{ }, }; use log::{error, info, warn, Level, Metadata, Record}; -use std::collections::vec_deque::VecDeque; -use std::collections::{HashMap, HashSet}; -use std::io::stdout; -use std::sync::{Arc, Mutex}; -use tokio::sync::mpsc; -use tokio::sync::oneshot; -use tokio_stream::StreamExt; -use tui::{ - backend::{Backend, CrosstermBackend}, +use ratatui::{ + backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style}, widgets::{ @@ -30,6 +23,13 @@ use tui::{ }, Frame, Terminal, }; +use std::collections::vec_deque::VecDeque; +use std::collections::{HashMap, HashSet}; +use std::io::stdout; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_stream::StreamExt; pub enum UIEvent { Connected(u16), @@ -301,7 +301,7 @@ impl UI { Ok(code) } - fn render_connected(&mut self, frame: &mut Frame) { + fn render_connected(&mut self, frame: &mut Frame) { let constraints = if self.show_logs { vec![Constraint::Percentage(50), Constraint::Percentage(50)] } else { @@ -311,7 +311,7 @@ impl UI { let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints) - .split(frame.size()); + .split(frame.area()); self.render_ports(frame, chunks[0]); if self.show_logs { @@ -322,7 +322,7 @@ impl UI { } } - fn render_ports(&mut self, frame: &mut Frame, size: Rect) { + fn render_ports(&mut self, frame: &mut Frame, size: Rect) { let enabled_port_style = Style::default(); let disabled_port_style = Style::default().fg(Color::DarkGray); let broken_port_style = @@ -358,17 +358,16 @@ impl UI { Constraint::Length(size.width), ]; - let port_list = Table::new(rows) + let port_list = Table::new(rows, &widths) .header(Row::new(vec!["fwd", "Port", "Description"])) .block(Block::default().title("Ports").borders(Borders::ALL)) .column_spacing(1) - .widths(&widths) .highlight_symbol(">> "); frame.render_stateful_widget(port_list, size, &mut self.selection); } - fn render_help(&mut self, frame: &mut Frame) { + fn render_help(&mut self, frame: &mut Frame) { let keybindings = vec![ Row::new(vec!["↑ / k", "Move cursor up"]), Row::new(vec!["↓ / j", "Move cursor down"]), @@ -387,10 +386,10 @@ impl UI { let help_popup_area = centered_rect( 65, keybindings.len() as u16 + border_lines, - frame.size(), + frame.area(), ); let inner_area = - help_popup_area.inner(&Margin { vertical: 1, horizontal: 1 }); + help_popup_area.inner(Margin { vertical: 1, horizontal: 1 }); let key_width = 7; let binding_width = inner_area.width.saturating_sub(key_width); @@ -398,8 +397,7 @@ impl UI { Constraint::Length(key_width), Constraint::Length(binding_width), ]; - let keybindings = Table::new(keybindings) - .widths(keybindings_widths) + let keybindings = Table::new(keybindings, keybindings_widths) .column_spacing(1) .block(Block::default().title("Keys").borders(Borders::ALL)); @@ -407,7 +405,7 @@ impl UI { frame.render_widget(keybindings, inner_area); } - fn render_logs(&mut self, frame: &mut Frame, size: Rect) { + fn render_logs(&mut self, frame: &mut Frame, size: Rect) { let items: Vec<_> = self.lines.iter().map(|l| ListItem::new(&l[..])).collect(); From 4fe255e7d26757a7c2c96c61d068c76a2a678e8b Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:52:20 -0700 Subject: [PATCH 64/85] Fix colors in the help box When the lines of the help box overlap with disabled or error'd ports you might notice that those lines are dark grey or red. That's surprising! The bug is that Style::default() means "don't change anything", just continue being whatever color the current cell is, which is deeply surprising. What we really want here is `Style::reset()`, which means "reset the colors to whatever the terminal would show by default." --- src/client/ui.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 4a3d2df..ae3fcd3 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -323,10 +323,10 @@ impl UI { } fn render_ports(&mut self, frame: &mut Frame, size: Rect) { - let enabled_port_style = Style::default(); - let disabled_port_style = Style::default().fg(Color::DarkGray); + let enabled_port_style = Style::reset(); + let disabled_port_style = Style::reset().fg(Color::DarkGray); let broken_port_style = - Style::default().fg(Color::Red).add_modifier(Modifier::DIM); + Style::reset().fg(Color::Red).add_modifier(Modifier::DIM); let mut rows = Vec::new(); let ports = self.get_ui_ports(); @@ -399,7 +399,8 @@ impl UI { ]; let keybindings = Table::new(keybindings, keybindings_widths) .column_spacing(1) - .block(Block::default().title("Keys").borders(Borders::ALL)); + .block(Block::default().title("Keys").borders(Borders::ALL)) + .style(Style::reset()); // keybindings frame.render_widget(keybindings, inner_area); From e44d4dea7aefca81ef08af93ffa38efb0139091b Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:56:29 -0700 Subject: [PATCH 65/85] Also update the fuzzing targets, I guess --- fuzz/Cargo.lock | 252 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 58 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 463a394..855731d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -136,6 +154,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.1.10" @@ -174,6 +201,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -220,16 +261,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.25.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", - "mio 0.8.11", + "mio", "parking_lot", + "rustix 0.38.34", "signal-hook", "signal-hook-mio", "winapi", @@ -276,6 +317,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -323,7 +370,7 @@ checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "fwd" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "bytes", @@ -332,16 +379,16 @@ dependencies = [ "env_logger", "home", "indoc", + "libc", "log", "open", "procfs", "rand", + "ratatui", "thiserror", "tokio", "tokio-stream", "toml", - "tui", - "users", "xdg", ] @@ -382,6 +429,22 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -438,6 +501,16 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -449,6 +522,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -466,9 +548,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -540,6 +622,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -573,18 +664,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.2" @@ -593,6 +672,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -683,6 +763,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -797,6 +883,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.5.3" @@ -839,6 +946,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -859,18 +972,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.206" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.206" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", @@ -906,7 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 0.8.11", + "mio", "signal-hook", ] @@ -980,6 +1093,34 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.74" @@ -1020,7 +1161,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1075,19 +1216,6 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags 1.3.2", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1100,6 +1228,17 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.13" @@ -1107,14 +1246,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] -name = "users" -version = "0.11.0" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" -dependencies = [ - "libc", - "log", -] +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1124,19 +1259,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -1149,9 +1285,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1159,9 +1295,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -1172,9 +1308,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wayland-backend" From 663ce420166a96493ee4abc2a5c06e2fab4498ee Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 13 Aug 2024 10:59:47 -0700 Subject: [PATCH 66/85] tempdir -> tempfile According to the documentation of the tempdir crate --- Cargo.lock | 79 +++++++++++---------------------------------- Cargo.toml | 2 +- src/reverse/unix.rs | 6 ++-- 3 files changed, 22 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 111ae68..4e43104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,6 +340,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "flate2" version = "1.0.31" @@ -350,12 +356,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures-core" version = "0.3.30" @@ -379,9 +379,9 @@ dependencies = [ "open", "pretty_assertions", "procfs", - "rand 0.8.5", + "rand", "ratatui", - "tempdir", + "tempfile", "thiserror", "tokio", "tokio-stream", @@ -830,19 +830,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -851,7 +838,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -861,24 +848,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -909,15 +881,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.5.3" @@ -927,15 +890,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1144,13 +1098,16 @@ dependencies = [ ] [[package]] -name = "tempdir" -version = "0.3.7" +name = "tempfile" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ - "rand 0.4.6", - "remove_dir_all", + "cfg-if", + "fastrand", + "once_cell", + "rustix 0.38.34", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0426ae9..2111a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ xdg = "2" [dev-dependencies] assert_matches = "1" pretty_assertions = "1" -tempdir = "0.3" +tempfile = "3" [target.'cfg(target_os="linux")'.dependencies] procfs = "0.14.1" diff --git a/src/reverse/unix.rs b/src/reverse/unix.rs index b85ed54..1d046b2 100644 --- a/src/reverse/unix.rs +++ b/src/reverse/unix.rs @@ -117,7 +117,7 @@ async fn handle_connection( mod tests { use super::*; use crate::message::MessageWriter; - use tempdir::TempDir; + use tempfile::TempDir; #[test] fn socket_path_repeats() { @@ -131,8 +131,8 @@ mod tests { async fn url_to_message() { let (sender, mut receiver) = mpsc::channel(64); - let tmp_dir = - TempDir::new("url_to_message").expect("Error getting tmpdir"); + let tmp_dir = TempDir::with_prefix("url_to_message") + .expect("Error getting tmpdir"); let path = tmp_dir.path().join("socket"); let path_override = path.clone(); From 38fbfbd918b4fb10f0db867c71c00e7e697fcc5d Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 10:51:19 -0700 Subject: [PATCH 67/85] Move config file to ~/.config/fwd/config.toml Presumably this also works for MacOS and windows. While doing this, move away from xdg and home and use this directories-next crate instead. Reverse connections still seem to work. --- Cargo.lock | 60 +++++++++++++++++++++++++++++++------------- Cargo.toml | 4 +-- src/client/config.rs | 12 +++++---- src/reverse/unix.rs | 10 ++++---- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e43104..3499c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,6 +290,27 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dlib" version = "0.5.2" @@ -371,8 +392,8 @@ dependencies = [ "bytes", "copypasta", "crossterm", + "directories-next", "env_logger", - "home", "indoc", "libc", "log", @@ -386,7 +407,6 @@ dependencies = [ "tokio", "tokio-stream", "toml", - "xdg", ] [[package]] @@ -450,15 +470,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "iana-time-zone" version = "0.1.60" @@ -561,6 +572,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -890,6 +911,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1679,12 +1711,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" -[[package]] -name = "xdg" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" - [[package]] name = "xkeysym" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 2111a12..c4c05c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ anyhow = "1.0" bytes = "1" copypasta = "0.10.1" crossterm = { version = "0.28.1", features = ["event-stream"] } +directories-next = "2" env_logger = { version = "0.11.5", default-features = false } -home = "0.5.4" indoc = "1" log = { version = "0.4", features = ["std"] } open = "3" @@ -29,7 +29,7 @@ thiserror = "1.0" tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] } tokio-stream = "0.1" toml = "0.5" -xdg = "2" + [dev-dependencies] assert_matches = "1" diff --git a/src/client/config.rs b/src/client/config.rs index f08220c..77d44e8 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -62,13 +62,15 @@ impl Config { pub fn load_config() -> Result { use std::io::ErrorKind; - let mut home = match home::home_dir() { - Some(h) => h, - None => return Ok(default()), + let Some(directories) = directories_next::ProjectDirs::from("", "", "fwd") + else { + return Ok(default()); }; - home.push(".fwd"); - let contents = match std::fs::read_to_string(home) { + let mut config_path = directories.config_dir().to_path_buf(); + config_path.push("config.toml"); + + let contents = match std::fs::read_to_string(config_path) { Ok(contents) => contents, Err(e) => match e.kind() { ErrorKind::NotFound => return Ok(default()), diff --git a/src/reverse/unix.rs b/src/reverse/unix.rs index 1d046b2..a968edd 100644 --- a/src/reverse/unix.rs +++ b/src/reverse/unix.rs @@ -49,11 +49,11 @@ pub fn socket_path() -> Result { } fn socket_directory() -> Result { - let base_directories = xdg::BaseDirectories::new() - .context("Error creating BaseDirectories")?; - match base_directories.place_runtime_file("fwd") { - Ok(path) => Ok(path), - Err(_) => { + match directories_next::ProjectDirs::from("", "", "fwd") + .and_then(|p| p.runtime_dir().map(|p| p.to_path_buf())) + { + Some(p) => Ok(p), + None => { let mut path = std::env::temp_dir(); let uid = unsafe { libc::getuid() }; path.push(format!("fwd{}", uid)); From afa13bf920dd17867e5fb6c1c050094876d92c0b Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 10:52:19 -0700 Subject: [PATCH 68/85] This description is out of date Given the introduction of anonymous ports --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7871d50..a6201e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,10 +19,10 @@ to send the the contents of `file`. Options: --version Print the version of fwd and exit --sudo, -s Run the server side of fwd with `sudo`. This allows the - client to forward ports that are open by processes being - run under other accounts (e.g., docker containers being - run as root), but requires sudo access on the server and - *might* end up forwarding ports that you do not want + client to identify the ports that are open by processes + being run under other accounts (e.g., docker containers + being run as root), but requires sudo access on the server + and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) --log-filter FILTER Set remote server's log level. Default is `warn`. Supports From cfde429786916509997c95c5d49698b869cf1ae4 Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 11:22:50 -0700 Subject: [PATCH 69/85] A man page, somewhat --- doc/fwd.man.md | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 doc/fwd.man.md diff --git a/doc/fwd.man.md b/doc/fwd.man.md new file mode 100644 index 0000000..992254c --- /dev/null +++ b/doc/fwd.man.md @@ -0,0 +1,150 @@ +% fwd(1) +% John Doty +% August 2024 + +# NAME + +fwd - Automatically forward connections to remote machines + +# SYNOPSIS + +**fwd** [OPTIONS] SERVER + +**fwd** [OPTIONS] browse URL + +**fwd** [OPTIONS] clip FILE + +**fwd-browse** URL + +# DESCRIPTION + +**fwd** enumerates the listening ports the remote server and automatically listens for connections on the same ports on the local machine. +When **fwd** receives a connection on the local machine, it automatically forwards that connection to the remote machine. + +**-s**, **-\-sudo** +: Run the server side of fwd with `sudo`. +: This allows the client to forward ports that are open by processes being run under other accounts (e.g., docker containers being run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.) + +**-\-log-filter** **FILTER** +: Set remote server's log level. Default is `warn`. +: Supports all of Rust's env_logger filter syntax, e.g. `--log-filter=fwd::trace`. + +**-\-version** +: Print the version of fwd and exit. + +# INTERACTIVE COMMANDS + +Once **fwd** is connected, it displays an interactive list of the ports available on the remote server. + +- Ports that **fwd** is listening on are displayed in the default terminal color. +- Ports that **fwd** is aware of but which are disabled are displayed in dark gray. +- Ports that **fwd** has tried to listen on but which have failed are displayed in red. + Details on the error may be found in the log window. + Disabling and re-enabling the port will cause **fwd** to try again. + +The following commands are available while **fwd** is connected: + +**Esc, q, Ctrl-C** +: Exit **fwd**. + +**?, h** +: Display the help window. + +**Up, k** +: Select the previous port in the list. + +**Down, j** +: Select the next port in the list. + +**Enter** +: Attempt to browse to localhost on the specified port with the default browser. + +**e** +: Enable or disable the selected port. + +**l** +: Show or hide the log window. + +# IDENTIFYING PORTS + +**fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. +It can identify ports in the following ways: + +*With docker* +: **fwd** will attempt to find and connect to a docker engine on the remote machine. +: If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container. + +*With procfs* +: On Linux, the listening ports are found by reading procfs and mapping them back to process command lines. +: **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine. + +Earlier methods take precedence over later methods. + +If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*. +Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration. + +# OPENING BROWSERS +**fwd** can be used to open URLs in the default browser on the local machine. +Run **fwd browse URL** on the remote server to open the `URL` in the default browser on the local machine. + +This only works if **fwd** is connected, and if the user running **fwd browse** is the same as the user that connected the **fwd** session. + +The **fwd-browse** program acts as a wrapper around **fwd browse**, to be used with configurations that can't handle a browser being a program with an argument. + +# CLIPBOARD +**fwd** can be used from the remote machine to place text on the clipboard of the local machine. +Run **fwd clip FILE** to copy the contents of the named file to the clipboard. +If **FILE** is **-**, this reads text from stdin instead. + +# CONFIGURATION +**fwd** can be configured with a configuration file. + +- On Windows, the config file will be in your roaming AppData folder. + (e.g., *c:\\Users\\Winifred\\AppData\\Roaming\\fwd\\config\\config.toml*) +- On MacOS, the config file will be in *$HOME/Library/Application Support/fwd/config.toml*. + (e.g., /Users/Margarie/Library/Application Support/fwd/config.toml) +- On XDG-ish systems (like Linux), the config file is in *~/.config/fwd/config.toml*. + (e.g., */home/lynette/.config/fwd/config.toml*) + +The following is an example of a *config.toml* file: + +``` +auto=true # should `fwd` should enable identified ports + +[servers.foo] # Server-specific settings for foo +auto=true +ports=[1080, 1082] # ports that are always present + +[servers.bar.ports] # `ports` can also be a table with port numbers as keys +1080=true # the values can be booleans (for enabled) + +[servers.bar.ports.1082] # port values can also be tables +enabled=true +description="A humble python" +``` + +Ports that are specified in the configuration file will always be present in the list of ports for a given server, even if no process is listening on that port. + +# TROUBLESHOOTING + +Connections are made via the **ssh** command. +Your **ssh** must: + +- Be on your path, so that **fwd** can find it to invoke it +- Be able to authenticate you to the remote server. + (Interactive authentication is fine.) +- Understand the **-D** command line option, to operate as a SOCKS5 server +- Be able to start the **fwd** command on the remote server + +A typical ssh invocation from **fwd** looks like: + +```bash +ssh -T -D XXXX me@server FWD_LOG=warning FWD_SEND_ANONYMOUS=1 fwd --server +``` + +**fwd** only enumerates ports that are listening on loopback addresses (e.g., 127.0.0.1) or on all addresses (e.g., 0.0.0.0). +If it cannot find a particular port, check to make sure that the process listening on that port is accessible via localhost. + +# SEE ALSO + +ssh(1) From 74e2da2f2941366e76a24bb0a7d63e29d7cb49ba Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 14 Aug 2024 11:24:23 -0700 Subject: [PATCH 70/85] Man page edits --- doc/fwd.man.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/fwd.man.md b/doc/fwd.man.md index 992254c..9f8f6da 100644 --- a/doc/fwd.man.md +++ b/doc/fwd.man.md @@ -70,15 +70,15 @@ The following commands are available while **fwd** is connected: **fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. It can identify ports in the following ways: -*With docker* +*docker* : **fwd** will attempt to find and connect to a docker engine on the remote machine. : If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container. -*With procfs* +*procfs* : On Linux, the listening ports are found by reading procfs and mapping them back to process command lines. : **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine. -Earlier methods take precedence over later methods. +(Earlier methods take precedence over later methods.) If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*. Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration. From 7a40326719d7dbdb76bbcf2b58132dee2bebcf62 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 10:14:43 -0700 Subject: [PATCH 71/85] Re-work config code Add raw description as a possible config for a port, and update the documentation appropriately. --- doc/fwd.man.md | 13 +- src/client/config.rs | 408 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 348 insertions(+), 73 deletions(-) diff --git a/doc/fwd.man.md b/doc/fwd.man.md index 9f8f6da..b7e5df9 100644 --- a/doc/fwd.man.md +++ b/doc/fwd.man.md @@ -109,14 +109,15 @@ If **FILE** is **-**, this reads text from stdin instead. The following is an example of a *config.toml* file: ``` -auto=true # should `fwd` should enable identified ports +auto=true # should `fwd` should enable identified ports (default true) -[servers.foo] # Server-specific settings for foo -auto=true -ports=[1080, 1082] # ports that are always present +[servers.foo] # Server-specific settings for foo +auto=true # defaults to the global setting +ports=[1080, 1082] # ports that are always present -[servers.bar.ports] # `ports` can also be a table with port numbers as keys -1080=true # the values can be booleans (for enabled) +[servers.bar.ports] # `ports` can also be a table with port numbers as keys +1080=true # the values can be booleans (for enabled)... +1081="My program" # or strings (for descriptions). [servers.bar.ports.1082] # port values can also be tables enabled=true diff --git a/src/client/config.rs b/src/client/config.rs index 77d44e8..e3a3cc0 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -1,15 +1,17 @@ use anyhow::{bail, Result}; use std::collections::hash_map; use std::collections::HashMap; -use toml::Value; +use toml::value::{Table, Value}; #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct PortConfig { pub enabled: bool, pub description: Option, } #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ServerConfig { auto: bool, ports: HashMap, @@ -45,6 +47,7 @@ impl ServerConfig { } #[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Config { auto: bool, servers: HashMap, @@ -85,85 +88,101 @@ fn default() -> Config { Config { auto: true, servers: HashMap::new() } } -fn parse_config(value: &Value) -> Result { - match value { - Value::Table(table) => Ok({ - let auto = match table.get("auto") { - None => true, - Some(Value::Boolean(v)) => *v, - Some(v) => bail!("expected a true or false, got {:?}", v), - }; - Config { auto, servers: get_servers(table, auto)? } - }), - _ => bail!("top level must be a table"), +fn get_bool(table: &Table, key: &str, default: bool) -> Result { + match table.get(key) { + None => Ok(default), + Some(Value::Boolean(v)) => Ok(*v), + Some(v) => bail!("expected a true or false, got {v:?}"), } } -fn get_servers( - table: &toml::value::Table, +fn parse_config(value: &Value) -> Result { + let Value::Table(table) = value else { + bail!("top level must be a table") + }; + + let auto = get_bool(table, "auto", true)?; + let servers = match table.get("servers") { + None => &Table::new(), + Some(Value::Table(t)) => t, + Some(v) => bail!("Expected a table in the servers key, got {v:?}"), + }; + + Ok(Config { + auto, + servers: parse_servers(servers, auto)?, + }) +} + +fn parse_servers( + table: &Table, auto: bool, ) -> Result> { - match table.get("servers") { - None => Ok(HashMap::new()), - Some(Value::Table(table)) => Ok({ - let mut servers = HashMap::new(); - for (k, v) in table { - servers.insert(k.clone(), get_server(v, auto)?); - } - servers - }), - v => bail!("expected a table in the servers key, got {:?}", v), + let mut servers = HashMap::new(); + for (k, v) in table { + let Value::Table(table) = v else { + bail!("expected a table for server {k}, got {v:?}"); + }; + + servers.insert(k.clone(), parse_server(table, auto)?); } + Ok(servers) } -fn get_server(value: &Value, auto: bool) -> Result { +fn parse_server(table: &Table, auto: bool) -> Result { + let auto = get_bool(table, "auto", auto)?; + let ports = match table.get("ports") { + None => HashMap::new(), + Some(v) => parse_ports(v)?, + }; + + Ok(ServerConfig { auto, ports }) +} + +fn parse_ports(value: &Value) -> Result> { match value { - Value::Table(table) => Ok(ServerConfig { - auto: match table.get("auto") { - None => auto, // Default to global default - Some(Value::Boolean(v)) => *v, - Some(v) => bail!("expected true or false, got {:?}", v), - }, - ports: get_ports(table)?, - }), - value => bail!("expected a table, got {:?}", value), - } -} - -fn get_ports(table: &toml::value::Table) -> Result> { - match table.get("ports") { - None => Ok(HashMap::new()), - Some(Value::Table(table)) => Ok({ - let mut ports = HashMap::new(); - for (k,v) in table { - let port:u16 = k.parse()?; - let config = match v { - Value::Boolean(enabled) => PortConfig{enabled:*enabled, description:None}, - Value::Table(table) => PortConfig{ - enabled: match table.get("enabled") { - Some(Value::Boolean(enabled)) => *enabled, - _ => bail!("not implemented"), - }, - description: match table.get("description") { - Some(Value::String(desc)) => Some(desc.clone()), - Some(v) => bail!("expect a string description, got {:?}", v), - None => None, - }, - }, - _ => bail!("expected either a boolean (enabled) or a table for a port config, got {:?}", v), - }; - ports.insert(port, config); - } - ports - }), - Some(Value::Array(array)) => Ok({ + Value::Array(array) => { let mut ports = HashMap::new(); for v in array { - ports.insert(get_port_number(v)?, PortConfig{enabled:true, description:None}); + ports.insert( + get_port_number(v)?, + PortConfig { enabled: true, description: None }, + ); } - ports + Ok(ports) + } + + Value::Table(table) => { + let mut ports = HashMap::new(); + for (k, v) in table { + let port: u16 = k.parse()?; + let config = parse_port_config(v)?; + ports.insert(port, config); + } + Ok(ports) + } + + _ => bail!("ports must be either an array or a table, got {value:?}"), + } +} + +fn parse_port_config(value: &Value) -> Result { + match value { + Value::Boolean(enabled) => Ok(PortConfig{enabled:*enabled, description:None}), + Value::String(description) => Ok(PortConfig{ + enabled: true, + description: Some(description.clone()), }), - Some(v) => bail!("ports must be either a table of ' = ...' or an array of ports, got {:?}", v), + Value::Table(table) => { + let enabled = get_bool(table, "enabled", true)?; + let description = match table.get("description") { + Some(Value::String(desc)) => Some(desc.clone()), + Some(v) => bail!("expect a string description, got {v:?}"), + None => None, + }; + Ok(PortConfig { enabled, description }) + }, + _ => bail!("expected either a boolean (enabled), a string (description), or a table for a port config, got {value:?}"), } } @@ -174,3 +193,258 @@ fn get_port_number(v: &Value) -> Result { }; Ok(port) } + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + fn config_test(config: &str, expected: Config) { + let config = config.parse::().expect("case not toml"); + let config = parse_config(&config).expect("unable to parse config"); + + assert_eq!(expected, config); + } + + fn config_error_test(config: &str) { + let config = config.parse::().expect("case not toml"); + assert!(parse_config(&config).is_err()); + } + + #[test] + fn empty() { + config_test("", Config { auto: true, servers: HashMap::new() }); + } + + #[test] + fn auto_false() { + config_test( + " +auto=false +", + Config { auto: false, servers: HashMap::new() }, + ); + } + + #[test] + fn auto_not_boolean() { + config_error_test( + " +auto='what is going on' +", + ); + } + + #[test] + fn servers_not_table() { + config_error_test("servers=1234"); + } + + #[test] + fn servers_default() { + config_test("servers.foo={}", { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: true, ports: HashMap::new() }, + ); + Config { auto: true, servers } + }) + } + + #[test] + fn servers_auto_false() { + config_test( + " +[servers.foo] +auto=false +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: false, ports: HashMap::new() }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_auto_not_bool() { + config_error_test( + " +[servers.foo] +auto=1234 +", + ) + } + + #[test] + fn servers_ports_list() { + config_test( + " +[servers.foo] +ports=[1,2,3] +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 2, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 3, + PortConfig { enabled: true, description: None }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_table_variations() { + config_test( + " +[servers.foo.ports] +1=true +2={enabled=false} +3=false +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { enabled: true, description: None }, + ); + ports.insert( + 2, + PortConfig { + enabled: false, + description: None, + }, + ); + ports.insert( + 3, + PortConfig { + enabled: false, + description: None, + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_table_descriptions() { + config_test( + " +[servers.foo.ports] +1={enabled=false} +2={description='humble'} +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { + enabled: false, + description: None, + }, + ); + ports.insert( + 2, + PortConfig { + enabled: true, + description: Some("humble".to_string()), + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_ports_raw_desc() { + config_test( + " +[servers.foo.ports] +1='humble' +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { + auto: true, + ports: { + let mut ports = HashMap::new(); + ports.insert( + 1, + PortConfig { + enabled: true, + description: Some("humble".to_string()), + }, + ); + ports + }, + }, + ); + Config { auto: true, servers } + }, + ) + } + + #[test] + fn servers_inherit_auto() { + config_test( + " +auto=false +servers.foo={} +", + { + let mut servers = HashMap::new(); + servers.insert( + "foo".to_string(), + ServerConfig { auto: false, ports: HashMap::new() }, + ); + Config { auto: false, servers } + }, + ) + } +} From 9c9f7cfa82750a70471f3bdc97d4d685d4219999 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:32:36 -0700 Subject: [PATCH 72/85] Release automation There are a lot of steps in preparing the release and so I'm trying to make sure that we're in a place where I can iterate on it locally. --- release.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 release.py diff --git a/release.py b/release.py new file mode 100644 index 0000000..b3c8546 --- /dev/null +++ b/release.py @@ -0,0 +1,128 @@ +"""A script to automate building and uploading a release archive. + +This is in python instead of bash because I abhor bash. Even though it's a +little nicer for running commands, it's worse at everything else. +""" + +import dataclasses +import os +import os.path +import pathlib +import shutil +import subprocess + +RELEASE_TAG = os.getenv("RELEASE_TAG") + +BUILD = os.getenv("BUILD") +if BUILD is None: + raise Exception("you *must* set the BUILD environment variable") + + +@dataclasses.dataclass +class BuildSettings: + target: str + test: bool = True + man_page: bool = True + strip: bool = True + windows: bool = False + ext: str = "" + + +print(f"doing release: {BUILD}") +build = { + "linux": BuildSettings( + target="x86_64-unknown-linux-musl", + ), + "macos": BuildSettings( + target="x86_64-apple-darwin", + ), + "arm-macos": BuildSettings( + target="aarch64-apple-darwin", + ), + "windows": BuildSettings( + target="x86_64-pc-windows-msvc", + strip=False, + man_page=False, + windows=True, + ext=".exe", + ), +}[BUILD] + +print(f"settings: {build}") + + +target_dir = pathlib.Path("target") / build.target / "release" +bins = [(target_dir / bin).with_suffix(build.ext) for bin in ["fwd", "fwd-browse"]] + + +def build_and_test(staging: pathlib.Path): + # Tools + subprocess.run( + ["rustup", "target", "add", build.target], + check=True, + ) + + # Test...? + if build.test: + subprocess.run( + ["cargo", "test", "--verbose", "--release", "--target", build.target], + check=True, + ) + + # Build + subprocess.run( + ["cargo", "build", "--verbose", "--release", "--target", build.target], + check=True, + ) + + # Strip + if build.strip: + for bin in bins: + subprocess.run(["strip", bin], check=True) + + # Copy + for bin in bins: + shutil.copyfile(bin, os.path.join(staging, os.path.basename(bin))) + + +def build_docs(staging: pathlib.Path): + shutil.copyfile("README.md", staging / "README.md") + if build.man_page: + print("Creating man page...") + proc = subprocess.run( + ["pandoc", "-s", "-tman", os.path.join("doc", "fwd.man.md")], + check=True, + capture_output=True, + encoding="utf8", + ) + contents = proc.stdout + with open(staging / "fwd.1", "w", encoding="utf-8") as f: + f.write(contents) + + +staging = pathlib.Path(f"fwd-{build.target}") +os.makedirs(staging, exist_ok=True) + +build_and_test(staging) +build_docs(staging) + +print("Creating archive...") +if build.windows: + archive = f"{staging}.zip" + subprocess.run(["7z", "a", archive, f"{staging}"], check=True) +else: + archive = f"{staging}.tar.gz" + subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) + +shutil.rmtree(staging) + +if RELEASE_TAG is None: + print("Not releasing to github, RELEASE_TAG is none.") +else: + print(f"Uploading {archive} to github release {RELEASE_TAG}...") + subprocess.run( + ["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"], + check=True, + ) + +os.unlink(archive) From 73126ba7706c08ca522de8562d8ad380a363f7ec Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:43:53 -0700 Subject: [PATCH 73/85] Update the release workflow Use the python automation script instead --- .github/workflows/release.yaml | 62 +++------------------------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4ef6b2f..b03aa0b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,10 +1,3 @@ -# From https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml -# Which is also via https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ -# ...both of which are very good. -# -# I'm sure I don't need half the stuff I have in here (around cargo -# customization and whatnot) but. -# name: release on: @@ -53,13 +46,6 @@ jobs: needs: ['create_release'] # We need to know the upload URL runs-on: ${{ matrix.os }} # We run many different builds env: - # For some builds, we use cross to test on 32-bit and big-endian - # systems. - CARGO: cargo - # When CARGO is set to CROSS, this is set to `--target matrix.target`. - TARGET_FLAGS: "" - # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. - TARGET_DIR: ./target # Emit backtraces on panics. RUST_BACKTRACE: 1 @@ -88,52 +74,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install Rust + - name: Install rust uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.target }} - - name: Use Cross + - name: Run the release automation shell: bash - run: | - cargo install cross - echo "CARGO=cross" >> $GITHUB_ENV - echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV - echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - - - name: Run the tests - run: ${{ env.CARGO }} test --verbose --release ${{ env.TARGET_FLAGS }} - - - name: Build release binary - run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} - - - name: Strip release binary (linux and macos) - if: matrix.build == 'linux' || matrix.build == 'macos' || matrix.build == 'arm-macos' - run: | - strip "target/${{ matrix.target }}/release/fwd" - strip "target/${{ matrix.target }}/release/fwd-browse" - - - name: Build archive - shell: bash - run: | - staging="fwd-${{ matrix.target }}" - mkdir -p "$staging" - - if [ "${{ matrix.os }}" = "windows-2022" ]; then - cp "target/${{ matrix.target }}/release/fwd.exe" "$staging/" - 7z a "$staging.zip" "$staging" - echo "ASSET=$staging.zip" >> $GITHUB_ENV - else - cp "target/${{ matrix.target }}/release/fwd" "$staging/" - cp "target/${{ matrix.target }}/release/fwd-browse" "$staging/" - tar czf "$staging.tar.gz" "$staging" - echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV - fi - - - name: Upload release archive env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - version="${{ needs.create_release.outputs.version }}" - gh release upload "$version" ${{ env.ASSET }} + RELEASE_TAG: ${{ needs.create_release.outputs.version }} + BUILD: ${{ matrix.build }} + run: python3 release.py From 666456e4561170fd39cc66a044b9ae017cc01a5c Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:50:24 -0700 Subject: [PATCH 74/85] More release stuff (tools) --- .github/workflows/release.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b03aa0b..acf1c6a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -74,6 +74,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install packages (linux) + if: matrix.build == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Install packages (macos) + if: matrix.build == 'macos' || matrix.build == 'arm-macos' + run: | + brew update + brew install pandoc + - name: Install rust uses: dtolnay/rust-toolchain@stable with: From 241e8e1eea4cb381a55065da00c3476c1fc67e06 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:52:50 -0700 Subject: [PATCH 75/85] This is broken because I have consummate vs --- .github/workflows/release.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index acf1c6a..91c222a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,14 +25,6 @@ jobs: run: | echo "version is: $VERSION" - - name: Check that tag version and Cargo.toml version are the same - shell: bash - run: | - if ! grep -q "version = \"$VERSION\"" Cargo.toml; then - echo "version does not match Cargo.toml" >&2 - exit 1 - fi - - name: Create GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2d1c8a4cebdfbee19eca19c0d1dc1e16078e2d71 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 15 Aug 2024 11:59:00 -0700 Subject: [PATCH 76/85] Set the github token for the release action --- .github/workflows/release.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 91c222a..b0b236f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -88,4 +88,5 @@ jobs: env: RELEASE_TAG: ${{ needs.create_release.outputs.version }} BUILD: ${{ matrix.build }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: python3 release.py From ff92002dcfad760447559ae339462f9d5dead95f Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:22:53 -0700 Subject: [PATCH 77/85] Update year I guess --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 7857f35..630f272 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2022 John Doty +Copyright 2024 John Doty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f13139e79bfaf448da162fba73aac5402514fe0f Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:23:44 -0700 Subject: [PATCH 78/85] Remove older documentation notes --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 1e3c07d..e0ac026 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A port-forwarding utility. Here's how it works: 1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd` -2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`) +2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`, or `.local/bin`) 3. You install `fwd` on the client (like your laptop) 4. You run `fwd` on the client to connect to the server, like so: @@ -21,9 +21,3 @@ If the port is something that might be interesting to a web browser, you can pre If something is going wrong, pressing `l` will toggle logs that might explain it. Press `q` to quit. - -## Future Improvements: - -- Clipboard integration: send something from the remote end of the pipe to the host's clipboard. (Sometimes you *really* want to copy some big buffer from the remote side and your terminal just can't make that work.) - -- Client heartbeats: I frequently wind up in a situation where the pipe is stalled: not broken but nothing is getting through. (This happens with my coder.com pipes all the time.) From 940e57346883c1bad485c726d5d0aea2e5885f4a Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:23:57 -0700 Subject: [PATCH 79/85] Support for debian packaging --- Cargo.toml | 24 +++++++++++++++++-- release.py | 69 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c4c05c8..7e9f5ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,14 @@ [package] name = "fwd" version = "0.9.2" +authors = ["John Doty "] edition = "2021" license = "MIT" -description = "Automatically forward ports to a remote server over ssh" +description = "Automatically forward ports to a remote server" +readme = "README.md" +documentation = "https://github.com/DeCarabas/fwd" homepage = "https://github.com/DeCarabas/fwd" repository = "https://github.com/DeCarabas/fwd" -readme = "README.md" [[bin]] name = "fwd-browse" @@ -41,3 +43,21 @@ procfs = "0.14.1" [target.'cfg(target_family="unix")'.dependencies] libc = "0.2.155" + +[package.metadata.deb] +section = "net" +depends = [] # No auto deps? +assets = [ + ["target/release/fwd", "usr/bin/", "755"], + ["LICENSE", "usr/share/doc/fwd/", "644"], + ["README.md", "usr/share/doc/fwd/README", "644"], + # The man page is automatically generated by fwd's build process. See + # release.py for details. + ["target/release/fwd.1", "usr/share/man/man1/fwd.1", "644"], +] +extended-description = """\ +fwd enumerates the listening ports the remote server and automatically listens +for connections on the same ports on the local machine. When fwd receives a +connection on the local machine, it automatically forwards that connection to +the remote machine. +""" diff --git a/release.py b/release.py index b3c8546..381a2a7 100644 --- a/release.py +++ b/release.py @@ -5,6 +5,7 @@ little nicer for running commands, it's worse at everything else. """ import dataclasses +import enum import os import os.path import pathlib @@ -18,14 +19,20 @@ if BUILD is None: raise Exception("you *must* set the BUILD environment variable") +class Archive(enum.Enum): + TARBALL = 1 + ZIP = 2 + DEB = 3 + + @dataclasses.dataclass class BuildSettings: - target: str - test: bool = True - man_page: bool = True - strip: bool = True - windows: bool = False - ext: str = "" + target: str # The rust target to build for + test: bool = True # Whether or not to run tests + man_page: bool = True # Whether or not to generate a man page + strip: bool = True # Whether or not to strip binaries + archive: Archive = Archive.TARBALL # Archive type + ext: str = "" # The file extension of the binary print(f"doing release: {BUILD}") @@ -33,6 +40,11 @@ build = { "linux": BuildSettings( target="x86_64-unknown-linux-musl", ), + "deb": BuildSettings( + target="x86_64-unknown-linux-musl", + test=False, + archive=Archive.DEB, + ), "macos": BuildSettings( target="x86_64-apple-darwin", ), @@ -43,7 +55,7 @@ build = { target="x86_64-pc-windows-msvc", strip=False, man_page=False, - windows=True, + archive=Archive.ZIP, ext=".exe", ), }[BUILD] @@ -100,29 +112,50 @@ def build_docs(staging: pathlib.Path): f.write(contents) +def build_archive(staging: pathlib.Path) -> pathlib.Path: + print("Creating archive...") + if build.archive == Archive.ZIP: + archive = pathlib.Path(f"{staging}.zip") + subprocess.run(["7z", "a", archive, f"{staging}"], check=True) + + elif build.archive == Archive.DEB: + subprocess.run(["cargo", "install", "cargo-deb"], check=True) + shutil.copyfile(staging / "fwd.1", target_dir / "fwd.1") + subprocess.run(["cargo", "deb", "--target", build.target], check=True) + + # Knowing the deb path means knowing the target version but I don't + # actually have the version here. (Or, like, I have the release tag + # but not in testing.) So just find the hopefully singular .deb that + # we made. + deb_path = pathlib.Path("target") / build.target / "debian" + archives = list(deb_path.glob("*.deb")) + assert len(archives) == 1 + archive = archives[0] + + else: + assert build.archive == Archive.TARBALL + archive = pathlib.Path(f"{staging}.tar.gz") + subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) + + return archive + + staging = pathlib.Path(f"fwd-{build.target}") os.makedirs(staging, exist_ok=True) build_and_test(staging) build_docs(staging) - -print("Creating archive...") -if build.windows: - archive = f"{staging}.zip" - subprocess.run(["7z", "a", archive, f"{staging}"], check=True) -else: - archive = f"{staging}.tar.gz" - subprocess.run(["tar", "czf", archive, f"{staging}"], check=True) +archive = build_archive(staging) shutil.rmtree(staging) +assert archive.exists() if RELEASE_TAG is None: - print("Not releasing to github, RELEASE_TAG is none.") + print(f"Not releasing {archive} to github, RELEASE_TAG is none.") else: print(f"Uploading {archive} to github release {RELEASE_TAG}...") subprocess.run( ["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"], check=True, ) - -os.unlink(archive) + os.unlink(archive) From 9e8fa4d0a6f4d473f2fbf69d0dd3515f679d8733 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:26:50 -0700 Subject: [PATCH 80/85] Add debian build to release matrix --- .github/workflows/release.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b0b236f..2f508d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -44,36 +44,45 @@ jobs: strategy: fail-fast: false matrix: - build: ['linux', 'macos', 'arm-macos', 'windows'] + build: ['linux', 'debian', 'macos', 'arm-macos', 'windows'] include: - build: linux os: ubuntu-22.04 target: x86_64-unknown-linux-musl + packages: apt + + - build: debian + os: ubuntu-22.04 + target: x86_64-unknown-linux-musl + packages: apt - build: macos os: macos-latest target: x86_64-apple-darwin + packages: brew - build: arm-macos os: macos-latest target: aarch64-apple-darwin + packages: brew - build: windows os: windows-2022 target: x86_64-pc-windows-msvc + packages: none steps: - name: Checkout code uses: actions/checkout@v4 - name: Install packages (linux) - if: matrix.build == 'linux' + if: matrix.packages == 'apt' run: | sudo apt-get update sudo apt-get install -y pandoc - name: Install packages (macos) - if: matrix.build == 'macos' || matrix.build == 'arm-macos' + if: matrix.packages == 'brew' run: | brew update brew install pandoc From a4df8fc588d3fed5de908563b37a263f7cfac040 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:29:37 -0700 Subject: [PATCH 81/85] This is a better name for the build --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index 381a2a7..0fa912b 100644 --- a/release.py +++ b/release.py @@ -40,7 +40,7 @@ build = { "linux": BuildSettings( target="x86_64-unknown-linux-musl", ), - "deb": BuildSettings( + "debian": BuildSettings( target="x86_64-unknown-linux-musl", test=False, archive=Archive.DEB, From 3430cae95740ce5a446647ec7ed691bd4867bc23 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 16 Aug 2024 10:36:56 -0700 Subject: [PATCH 82/85] Add fwd-browse to the debian package --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 7e9f5ac..bed9327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ section = "net" depends = [] # No auto deps? assets = [ ["target/release/fwd", "usr/bin/", "755"], + ["target/release/fwd-browse", "usr/bin/", "755"], ["LICENSE", "usr/share/doc/fwd/", "644"], ["README.md", "usr/share/doc/fwd/README", "644"], # The man page is automatically generated by fwd's build process. See From eede5b0e5061763c267ec7e8aa24bf5e4b010d8a Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:03:41 -0700 Subject: [PATCH 83/85] Support showing and hiding anonymous ports I'm still not convinced that showing a big list of disabled ports is the right thing to do so here's the ability to turn it off. --- doc/fwd.man.md | 5 ++ src/client/ui.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/doc/fwd.man.md b/doc/fwd.man.md index b7e5df9..5d5f819 100644 --- a/doc/fwd.man.md +++ b/doc/fwd.man.md @@ -59,12 +59,17 @@ The following commands are available while **fwd** is connected: **Enter** : Attempt to browse to localhost on the specified port with the default browser. +**a** +: Hide or show anonymous ports. +: (See "identifying ports" below for more information on anonymous ports.) + **e** : Enable or disable the selected port. **l** : Show or hide the log window. + # IDENTIFYING PORTS **fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port. diff --git a/src/client/ui.rs b/src/client/ui.rs index ae3fcd3..bb5b36d 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -155,6 +155,16 @@ impl Listener { "" } + pub fn is_anonymous(&self) -> bool { + // Anonynous ports are not configured and came from the server but + // had no description there. + self.config.is_none() + && match self.desc.as_ref() { + Some(desc) => desc.desc.is_empty(), + None => false, + } + } + fn state(&self) -> State { *self.state.lock().unwrap() } @@ -223,6 +233,7 @@ pub struct UI { show_help: bool, alternate_screen: bool, raw_mode: bool, + show_anonymous: bool, clipboard: Option, } @@ -247,6 +258,7 @@ impl UI { config, alternate_screen: false, raw_mode: false, + show_anonymous: true, clipboard, } } @@ -334,6 +346,10 @@ impl UI { ports.iter().map(|p| format!("{p}")).collect(); for (index, port) in ports.into_iter().enumerate() { let listener = self.ports.get(&port).unwrap(); + if !self.should_render_listener(listener) { + continue; + } + let (symbol, style) = match listener.state() { State::Enabled => (" ✓ ", enabled_port_style), State::Broken => (" ✗ ", broken_port_style), @@ -379,6 +395,7 @@ impl UI { Row::new(vec!["ESC / q", "Quit"]), Row::new(vec!["? / h", "Show this help text"]), Row::new(vec!["l", "Show fwd's logs"]), + Row::new(vec!["a", "Hide/show anonymous ports"]), ]; let border_lines = 3; @@ -477,6 +494,19 @@ impl UI { Ok(()) } + fn toggle_show_anonymous(&mut self) { + self.show_anonymous = !self.show_anonymous; + } + + fn should_render_listener(&self, listener: &Listener) -> bool { + // Named/Configured ports are always rendered + !listener.is_anonymous() + // ...or we might be explicitly asked to render everything + || self.show_anonymous + // ...or the port might be enabled or errored + || listener.state() != State::Disabled + } + async fn handle_events(&mut self, console_events: &mut EventStream) { tokio::select! { ev = console_events.next() => self.handle_console_event(ev), @@ -584,6 +614,10 @@ impl UI { _ = open::that(format!("http://127.0.0.1:{}/", p)); } } + KeyEvent { code: KeyCode::Char('a'), .. } => { + self.toggle_show_anonymous() + } + _ => (), }, Some(Ok(_)) => (), // Don't care about this event... @@ -1250,4 +1284,118 @@ mod tests { drop(sender); } + + #[test] + fn listener_anonymous() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8079, + PortConfig { + enabled: false, + description: Some("body once told me".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { + port: 8080, + desc: "python3 blaster.py".to_string(), + }, + PortDesc { port: 8081, desc: "".to_string() }, + PortDesc { port: 8082, desc: "".to_string() }, + ]))); + + // (Pretend that 8082 broke.) + ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed(); + + let listener = ui.ports.get(&8079).unwrap(); + assert!( + !listener.is_anonymous(), + "Configured ports should not be anonymous" + ); + + let listener = ui.ports.get(&8080).unwrap(); + assert!( + !listener.is_anonymous(), + "Ports with descriptions should not be anonymous" + ); + + let listener = ui.ports.get(&8081).unwrap(); + assert!( + listener.is_anonymous(), + "Not configured, disabled, no description should be anonymous" + ); + + drop(sender); + } + + #[test] + fn render_anonymous() { + let (sender, receiver) = mpsc::channel(64); + let mut config = ServerConfig::default(); + config.insert( + 8079, + PortConfig { + enabled: false, + description: Some("body once told me".to_string()), + }, + ); + + let mut ui = UI::new(receiver, config); + + ui.handle_internal_event(Some(UIEvent::Ports(vec![ + PortDesc { + port: 8080, + desc: "python3 blaster.py".to_string(), + }, + PortDesc { port: 8081, desc: "".to_string() }, + PortDesc { port: 8082, desc: "".to_string() }, + PortDesc { port: 8083, desc: "".to_string() }, + ]))); + + // (Pretend that 8082 broke.) + ui.ports.get_mut(&8082).unwrap().state = State::Broken.boxed(); + + // No showing anonymous ports! + ui.show_anonymous = false; + + let listener = ui.ports.get(&8079).unwrap(); + assert!( + ui.should_render_listener(listener), + "Configured ports should always be rendered" + ); + + let listener = ui.ports.get(&8080).unwrap(); + assert!( + ui.should_render_listener(listener), + "Ports with descriptions should be rendered" + ); + + let listener = ui.ports.get(&8081).unwrap(); + assert!( + !ui.should_render_listener(listener), + "Not configured, disabled, no description should be hidden" + ); + + ui.enable_disable_port(8081); + + let listener = ui.ports.get(&8081).unwrap(); + assert_eq!(listener.state(), State::Enabled); + assert!( + ui.should_render_listener(listener), + "Enabled ports should be rendered" + ); + + let listener = ui.ports.get(&8082).unwrap(); + assert_eq!(listener.state(), State::Broken); + assert!( + ui.should_render_listener(listener), + "Broken ports should be rendered" + ); + + drop(sender); + } } From aad9c74a63d0f0640c6766d5553278b694f28841 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:29:27 -0700 Subject: [PATCH 84/85] Vanity --- README.md | 3 +++ doc/screenshot-01.png | Bin 0 -> 85053 bytes src/client/ui.rs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 doc/screenshot-01.png diff --git a/README.md b/README.md index e0ac026..c062243 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ doty@my.laptop$ fwd some.server ``` `fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally. + +A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. + Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port. Now, connections to that port locally will be forwarded to the remote server. diff --git a/doc/screenshot-01.png b/doc/screenshot-01.png new file mode 100644 index 0000000000000000000000000000000000000000..4f5315fc2dbbcaea337f9a36788d1649b51c198e GIT binary patch literal 85053 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 ZRKr^#uXVA|QR!(L*@VwRDl?;waQyH8KbQroqoq zk$ln56~>mbYxT4ioqOZzIp!sBxotxl)3o<@H1k z_|SXzMy+1LMs}}2dO5gFH*10LZP7jf2@aX=(_~SZ>wVia3)nMxH~DWXE4x2DV-m}G zPL8|3bmfjKxyj!di!=9_5KdMz`QkQ5h3+dQMdSOhVF_q;CwUGaCkZ`>(Ce_kWHhQU zh^^dnfWFh~*DaC(<}%P2_K3h-AtJ00>Y>2Mzwj6^byos}@ZF1rH%DaD-c}&`*6RQBky1J2gHx>{yNTU&f$=+mU$FJ(v;Q-QXjo2%mdLz z39w?sLF64F;8q5)A>^qnJqD)Tm)l!P81NLbKu#umJ8ER>y2R(eY|wA3@Pbxj)VU)^NxZ zwo^k6yVD~t$Cx}c+yuyP;2g47eD2Xb*I+=1IzR277t_*UP|Z3iQPFh&QBg4v=KOWX zQjqR;VaIgd2mApFDL-MozU|$OUOiAWZn3bG+j7A9JxHkQY5k-Y$C~53&Lb>xkW@b4 zr5%%^+XQ-;k |LwYtkOLlqd7EmoVSpAtx&!BJlh#jvuh`%oeJDRvSEYiQ;o|T#j zcf~iLj|?3`e3@PDDl#VhQoU?GUpTC$3xB56yGfL_u@`?oX7b%QO-lthX)^Wihy5(! z4_vR#ZLuCExaKr8HC`kV7B!25jVE@DwLZv_H@(#Nfmt~daaedyQa-;kMssD6=6*o1 z=Vo}+{j4zo6Ob@sZC?~V)I0FPpw-KVK@Sonz<%zOG1CpxdK0C?!7Wr)Cmm5n!6l@k zr$7G}3-B&BxGUF@UyvPt5xC~_Fo&$4iQ2oGm~g`E4f$pbk8D55%f*8B{A6ViL1BOVPSWUGOhrKp&ivM z>{SqP{&fHH#+6~9wNdY8z(u?LddE)Bv&`WUwRP*U(zIwTOmq-X@!yvK(hXO1-Ggj( zgEZAKf;HsPy+IfMS^3BROUj=fpJG1jd-ThQ={crsWo|MG%M}liN~BxE%5}h@V`^j0 zf5ljsp8Y3`MK6bqk2KcE#YN`@@ouat4_1|q3m$(P!isAEncbCa;((6-rzL5UwUuZj z;f8F#Y5VLXEo$LeXUCADRN4`I5+x4DgcS?DPM3=4FTaO`A3v3iyw;lx9SKY2PXuw` z_IUL=&{?aXXKR7Z!se@|snu(DX?9iW#-b+-`0>Y&nSGs$k8X~)4~$h;Eu3EbEyi-~ zbC%hQ)If7Ao10vfbliKfU2aj>&&Z=lp1bdJSCquZJ!r-SaRdF_2X9Ac;UO(`o#(al znaKfal*pC=lz|)e!**D=$;cVnqrSrde1qt7i!j6FYj@+{HhiPZkhXVBNFq#qI=fMP za-Uu(=GSt*d@&M3mlpaBaN_EzOu$D9;9k`%WYki^i(NJ&K*9w4lS4nZK<4Iw5K~7h z^0&65@cEm=MXPDWdUc%~Od_Qi`fUawR7d|Rq5AE+%M+I0{Dy5$$XSb4GL0h8D_q4D}i6Z&M|!wZHLqtPgridX3G-1C~IP^_p~wmZ34wh|TTBm1rF%(C9wG?aV>(W&3l zfjVHlO@T=E9ooBa-&#~HrS$!>Ue%bX(Hc}P8OogYDTr|nz)EQ zwuZPC2*Humdkduhkgjv!gn>t|D48~ciVLyyf>M*__dEYVo}0dz6ml$v@VmLXTfgZ` zbYl~KOZ;KcCs61A59adyzr$S6v_Pa#j7*}I%K+;-DS948)|r=^`s9xU-dSgD>nLJr zX|CaDuauV2f&-WU(t3pG*y?4EgLhRnMJan-5;J3_B z;l6{3Mqgm@Ou*}t$vr|+1)^U<9$jA8<^3;JfhIzlFhB;$xbqF6H}~&(F2)2!e@n

L*_N^*QPQ3dUQ!@(WaGiH>dr=^^a;wF{e^jvL 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 literal 0 HcmV?d00001 diff --git a/src/client/ui.rs b/src/client/ui.rs index bb5b36d..c29f86a 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -479,7 +479,7 @@ impl UI { fn enter_alternate_screen(&mut self) -> Result<()> { if !self.alternate_screen { enable_raw_mode()?; - execute!(stdout(), EnterAlternateScreen, DisableLineWrap)?; + execute!(stdout(), EnterAlternateScreen, DisableLineWrap,)?; self.alternate_screen = true; } Ok(()) From 3f6edc7662c4e69247ed1fa36fc1660d89663452 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:30:41 -0700 Subject: [PATCH 85/85] Hubris --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c062243..94a4337 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ doty@my.laptop$ fwd some.server `fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally. -A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. +A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted. Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port. Now, connections to that port locally will be forwarded to the remote server.