From eede5b0e5061763c267ec7e8aa24bf5e4b010d8a Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 17 Aug 2024 08:03:41 -0700 Subject: [PATCH] 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); + } }