diff --git a/Cargo.lock b/Cargo.lock index 81da77a..3d6192e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,7 +98,7 @@ dependencies = [ "log", "object", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -112,7 +112,7 @@ dependencies = [ "hashbrown", "log", "object", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -334,23 +334,23 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -671,6 +671,7 @@ dependencies = [ "network-types", "oryx-common", "ratatui", + "regex", "serde", "serde_json", "tui-big-text", @@ -698,7 +699,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -767,13 +768,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] @@ -961,7 +962,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -975,6 +985,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tui-big-text" version = "0.7.0" @@ -1082,22 +1103,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[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", + "windows-targets", ] [[package]] @@ -1106,22 +1118,7 @@ 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.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", + "windows-targets", ] [[package]] @@ -1130,46 +1127,28 @@ 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_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "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", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[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.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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1182,48 +1161,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[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.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.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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Readme.md b/Readme.md index 839fdc6..8dc3702 100644 --- a/Readme.md +++ b/Readme.md @@ -11,6 +11,7 @@ - Real-time traffic inspection and visualization. - Comprehensive Traffic Statistics. - Firewall functionalities. +- Metrics explorer. - Fuzzy search. ## 💡 Prerequisites diff --git a/oryx-tui/Cargo.toml b/oryx-tui/Cargo.toml index af73fe3..2385697 100644 --- a/oryx-tui/Cargo.toml +++ b/oryx-tui/Cargo.toml @@ -18,7 +18,7 @@ aya = "0.13" oryx-common = { path = "../oryx-common" } mio = { version = "1", features = ["os-poll", "os-ext"] } itertools = "0.14" -dirs = "5" +dirs = "6" kanal = "0.1.0-pre8" mimalloc = "0.1" clap = { version = "4", features = ["derive", "cargo"] } @@ -28,6 +28,7 @@ log = "0.4" env_logger = "0.11" serde_json = "1" serde = { version = "1", features = ["derive"] } +regex = "1" [[bin]] name = "oryx" diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index 8906d80..a6883bb 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -28,6 +28,7 @@ pub enum ActivePopup { UpdateFilters, PacketInfos, NewFirewallRule, + NewMetricExplorer, } #[derive(Debug)] diff --git a/oryx-tui/src/handler.rs b/oryx-tui/src/handler.rs index dca3f05..f86019f 100644 --- a/oryx-tui/src/handler.rs +++ b/oryx-tui/src/handler.rs @@ -70,6 +70,10 @@ pub fn handle_key_events( .handle_keys(key_event, event_sender.clone())?; app.is_editing = false; } + ActivePopup::NewMetricExplorer => { + app.section.metrics.handle_popup_keys(key_event)?; + app.is_editing = false; + } _ => {} } } @@ -92,6 +96,12 @@ pub fn handle_key_events( app.is_editing = false; } } + ActivePopup::NewMetricExplorer => { + if app.section.metrics.handle_popup_keys(key_event).is_ok() { + app.active_popup = None; + app.is_editing = false; + } + } _ => {} }, @@ -104,6 +114,9 @@ pub fn handle_key_events( .firewall .handle_keys(key_event, event_sender.clone())?; } + ActivePopup::NewMetricExplorer => { + app.section.metrics.handle_popup_keys(key_event)?; + } _ => {} }, } @@ -158,11 +171,21 @@ pub fn handle_key_events( KeyCode::Char('n') | KeyCode::Char('e') => { if app.section.focused_section == FocusedSection::Firewall - && app.section.handle_keys(key_event, event_sender).is_ok() + && app + .section + .handle_keys(key_event, event_sender.clone()) + .is_ok() { app.is_editing = true; app.active_popup = Some(ActivePopup::NewFirewallRule); } + + if app.section.focused_section == FocusedSection::Metrics + && app.section.handle_keys(key_event, event_sender).is_ok() + { + app.is_editing = true; + app.active_popup = Some(ActivePopup::NewMetricExplorer); + } } KeyCode::Char('i') => { diff --git a/oryx-tui/src/section.rs b/oryx-tui/src/section.rs index e9db88e..ea23120 100644 --- a/oryx-tui/src/section.rs +++ b/oryx-tui/src/section.rs @@ -1,6 +1,7 @@ pub mod alert; pub mod firewall; pub mod inspection; +pub mod metrics; pub mod stats; use std::sync::{Arc, Mutex}; @@ -10,6 +11,7 @@ use crossterm::event::{KeyCode, KeyEvent}; use firewall::{Firewall, FirewallSignal}; use inspection::Inspection; +use metrics::Metrics; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Style, Stylize}, @@ -30,6 +32,7 @@ use crate::{ pub enum FocusedSection { Inspection, Stats, + Metrics, Alerts, Firewall, } @@ -39,6 +42,7 @@ pub struct Section { pub focused_section: FocusedSection, pub inspection: Inspection, pub stats: Option, + pub metrics: Metrics, pub alert: Alert, pub firewall: Firewall, } @@ -52,6 +56,7 @@ impl Section { focused_section: FocusedSection::Inspection, inspection: Inspection::new(packets.clone()), stats: None, + metrics: Metrics::new(packets.clone()), alert: Alert::new(packets.clone()), firewall: Firewall::new(firewall_chans.ingress.sender, firewall_chans.egress.sender), } @@ -79,6 +84,16 @@ impl Section { Span::from(" Stats 󱕍 ").fg(Color::DarkGray) } } + FocusedSection::Metrics => { + if is_focused { + Span::styled( + " Metrics  ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } else { + Span::from(" Metrics  ").fg(Color::DarkGray) + } + } FocusedSection::Alerts => self.alert.title_span(is_focused), FocusedSection::Firewall => { if is_focused { @@ -133,6 +148,13 @@ impl Section { Span::from(" ").bold(), Span::from(": Naviguate").bold(), ]), + Some(ActivePopup::NewMetricExplorer) => Line::from(vec![ + Span::from("󱊷 ").bold(), + Span::from(": Discard").bold(), + Span::from(" | ").bold(), + Span::from("󱞦 ").bold(), + Span::from(": Run").bold(), + ]), Some(ActivePopup::PacketInfos) | Some(ActivePopup::Help) => Line::from(vec![ Span::from("󱊷 ").bold(), Span::from(": Discard Popup").bold(), @@ -191,6 +213,25 @@ impl Section { Span::from(" ").bold(), Span::from(" Nav").bold(), ]), + FocusedSection::Metrics => Line::from(vec![ + Span::from("k,").bold(), + Span::from(" Up").bold(), + Span::from(" | ").bold(), + Span::from("j,").bold(), + Span::from(" Down").bold(), + Span::from(" | ").bold(), + Span::from("n").bold(), + Span::from(" New").bold(), + Span::from(" | ").bold(), + Span::from("d").bold(), + Span::from(" Delete").bold(), + Span::from(" | ").bold(), + Span::from("f").bold(), + Span::from(" Filters").bold(), + Span::from(" | ").bold(), + Span::from(" ").bold(), + Span::from(" Nav").bold(), + ]), _ => Line::from(vec![ Span::from("f").bold(), Span::from(" Filters").bold(), @@ -219,6 +260,7 @@ impl Section { Line::from(vec![ self.title_span(FocusedSection::Inspection), self.title_span(FocusedSection::Stats), + self.title_span(FocusedSection::Metrics), self.title_span(FocusedSection::Alerts), self.title_span(FocusedSection::Firewall), ]) @@ -232,6 +274,7 @@ impl Section { block, ); } + pub fn render( &mut self, frame: &mut Frame, @@ -259,6 +302,7 @@ impl Section { stats.render(frame, section_block, network_interace) } } + FocusedSection::Metrics => self.metrics.render(frame, section_block), FocusedSection::Alerts => self.alert.render(frame, section_block), FocusedSection::Firewall => self.firewall.render(frame, section_block), } @@ -272,7 +316,8 @@ impl Section { match key_event.code { KeyCode::Tab => match self.focused_section { FocusedSection::Inspection => self.focused_section = FocusedSection::Stats, - FocusedSection::Stats => self.focused_section = FocusedSection::Alerts, + FocusedSection::Stats => self.focused_section = FocusedSection::Metrics, + FocusedSection::Metrics => self.focused_section = FocusedSection::Alerts, FocusedSection::Alerts => self.focused_section = FocusedSection::Firewall, FocusedSection::Firewall => self.focused_section = FocusedSection::Inspection, }, @@ -280,7 +325,8 @@ impl Section { KeyCode::BackTab => match self.focused_section { FocusedSection::Inspection => self.focused_section = FocusedSection::Firewall, FocusedSection::Stats => self.focused_section = FocusedSection::Inspection, - FocusedSection::Alerts => self.focused_section = FocusedSection::Stats, + FocusedSection::Metrics => self.focused_section = FocusedSection::Stats, + FocusedSection::Alerts => self.focused_section = FocusedSection::Metrics, FocusedSection::Firewall => self.focused_section = FocusedSection::Alerts, }, @@ -291,6 +337,7 @@ impl Section { FocusedSection::Firewall => self .firewall .handle_keys(key_event, notification_sender.clone())?, + FocusedSection::Metrics => self.metrics.handle_keys(key_event), _ => {} }, } diff --git a/oryx-tui/src/section/metrics.rs b/oryx-tui/src/section/metrics.rs new file mode 100644 index 0000000..638ed69 --- /dev/null +++ b/oryx-tui/src/section/metrics.rs @@ -0,0 +1,484 @@ +use std::{ + cmp, + ops::Range, + sync::{atomic::AtomicBool, Arc, Mutex}, + thread, + time::Duration, +}; + +use crossterm::event::{Event, KeyCode, KeyEvent}; +use regex::Regex; +use tui_input::{backend::crossterm::EventHandler, Input}; + +use ratatui::{ + layout::{Alignment, Constraint, Direction, Flex, Layout, Margin, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Text}, + widgets::{ + Bar, BarChart, BarGroup, Block, BorderType, Borders, Cell, Clear, HighlightSpacing, + Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, + }, + Frame, +}; + +use crate::{ + app::AppResult, + packet::{ + direction::TrafficDirection, + network::{IpPacket, IpProto}, + AppPacket, NetworkPacket, + }, +}; + +#[derive(Debug, Default)] +struct ListState { + offset: usize, + selected: Option, +} + +#[derive(Debug)] +pub struct Metrics { + user_input: UserInput, + app_packets: Arc>>, + metrics: Vec>>, + terminate: Arc, + state: ListState, + window_height: usize, +} + +#[derive(Debug, Clone, Default)] +struct UserInput { + input: Input, + error: Option, +} + +impl UserInput { + fn validate(&mut self) -> AppResult> { + self.error = None; + let re = Regex::new(r"^(?\d{1,5})\-(?\d{1,5})$").unwrap(); + + match self.input.value().parse::() { + Ok(v) => Ok(Range { + start: v, + end: v + 1, + }), + Err(_) => { + let Some(caps) = re.captures(self.input.value()) else { + self.error = Some("Invalid Port(s)".to_string()); + return Err("Validation Error".into()); + }; + + let start: u16 = caps["start"].parse()?; + let end: u16 = caps["end"].parse()?; + + // Empty range + if start >= end { + self.error = Some("Invalid Port Range".to_string()); + return Err("Validation Error".into()); + } + + Ok(Range { start, end }) + } + } + } + + fn clear(&mut self) { + self.input.reset(); + self.error = None; + } +} + +#[derive(Debug, Default, Clone)] +pub struct PortCountMetric { + port_range: Range, + tcp_count: usize, + udp_count: usize, +} + +impl Metrics { + pub fn new(packets: Arc>>) -> Self { + Self { + user_input: UserInput::default(), + app_packets: packets, + metrics: Vec::new(), + terminate: Arc::new(AtomicBool::new(false)), + state: ListState::default(), + window_height: 0, + } + } + + pub fn render(&mut self, frame: &mut Frame, block: Rect) { + self.window_height = block.height.saturating_sub(4) as usize / 8; + + let constraints: Vec = (0..self.window_height) + .map(|_| Constraint::Length(8)) + .collect(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(block.inner(Margin { + horizontal: 0, + vertical: 2, + })); + + let blocks: Vec<_> = chunks + .iter() + .map(|b| { + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(90), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(*b)[1] + }) + .collect(); + + let metrics_to_display = if self.metrics.len() <= self.window_height { + self.metrics.clone() + } else { + self.metrics[self.state.offset..self.state.offset + self.window_height].to_vec() + }; + + for (index, port_count_metric) in metrics_to_display.iter().enumerate() { + let metric = { port_count_metric.lock().unwrap().clone() }; + + let chart = BarChart::default() + .direction(Direction::Horizontal) + .bar_width(1) + .bar_gap(1) + .data( + BarGroup::default().bars(&[ + Bar::default() + .label("TCP".into()) + .style(Style::new().fg(Color::LightYellow)) + .value(metric.tcp_count.try_into().unwrap()) + .value_style(Style::new().fg(Color::Black).bg(Color::LightYellow)) + .text_value(metric.tcp_count.to_string()), + Bar::default() + .label("UDP".into()) + .style(Style::new().fg(Color::LightBlue)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightBlue)) + .value(metric.udp_count.try_into().unwrap()) + .text_value(metric.udp_count.to_string()), + ]), + ) + .max((metric.udp_count + metric.tcp_count) as u64) + .block( + Block::new() + .title_alignment(Alignment::Center) + .borders(Borders::LEFT) + .border_style({ + if self.state.selected.unwrap() - self.state.offset == index { + Style::new().fg(Color::Magenta) + } else { + Style::new().fg(Color::Gray) + } + }) + .border_type({ + if self.state.selected.unwrap() - self.state.offset == index { + BorderType::Thick + } else { + BorderType::Plain + } + }) + .padding(Padding::uniform(1)) + .title_top({ + if metric.port_range.len() == 1 { + format!("Port: {}", metric.port_range.start) + } else { + format!( + "Ports: {}-{}", + metric.port_range.start, metric.port_range.end + ) + } + }), + ); + frame.render_widget( + chart, + blocks[index].inner(Margin { + horizontal: 0, + vertical: 1, + }), + ); + } + + if self.metrics.len() > self.window_height { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = ScrollbarState::new(self.metrics.len()) + .position(self.state.offset * self.window_height); + frame.render_stateful_widget( + scrollbar, + block.inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } + } + + pub fn handle_keys(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('d') => { + if self.metrics.is_empty() { + return; + } + if let Some(selected_item_index) = &mut self.state.selected { + self.terminate + .store(true, std::sync::atomic::Ordering::Relaxed); + + let _ = self.metrics.remove(*selected_item_index); + + self.user_input.clear(); + + self.state.selected = Some(selected_item_index.saturating_sub(1)); + + self.terminate + .store(false, std::sync::atomic::Ordering::Relaxed); + } + } + + KeyCode::Char('k') | KeyCode::Up => { + let i = match self.state.selected { + Some(i) => { + if i > self.state.offset { + i - 1 + } else if i == self.state.offset && self.state.offset > 0 { + self.state.offset -= 1; + i - 1 + } else { + 0 + } + } + None => 0, + }; + + self.state.selected = Some(i); + } + + KeyCode::Char('j') | KeyCode::Down => { + if self.metrics.is_empty() { + return; + } + let i = match self.state.selected { + Some(i) => { + if i < self.window_height - 1 { + cmp::min(i + 1, self.metrics.len() - 1) + } else if self.metrics.len() - 1 == i { + i + } else { + self.state.offset += 1; + i + 1 + } + } + None => 0, + }; + + self.state.selected = Some(i); + } + _ => {} + } + } + + pub fn handle_popup_keys(&mut self, key_event: KeyEvent) -> AppResult<()> { + match key_event.code { + KeyCode::Esc => { + self.user_input.clear(); + } + + KeyCode::Enter => { + let port_range: Range = self.user_input.validate()?; + + let port_count_metric = Arc::new(Mutex::new(PortCountMetric { + port_range: port_range.clone(), + tcp_count: 0, + udp_count: 0, + })); + + thread::spawn({ + let port_count_metric = port_count_metric.clone(); + let terminate = self.terminate.clone(); + let packets = self.app_packets.clone(); + move || { + let mut last_index = 0; + 'main: loop { + thread::sleep(Duration::from_millis(100)); + + let app_packets = { packets.lock().unwrap().clone() }; + + if app_packets.is_empty() { + continue; + } + let mut metric = port_count_metric.lock().unwrap(); + for app_packet in app_packets[last_index..].iter() { + if terminate.load(std::sync::atomic::Ordering::Relaxed) { + break 'main; + } + if app_packet.direction == TrafficDirection::Ingress { + if let NetworkPacket::Ip(packet) = app_packet.packet { + match packet { + IpPacket::V4(ipv4_packet) => match ipv4_packet.proto { + IpProto::Tcp(tcp_packet) => { + if port_range.contains(&tcp_packet.dst_port) { + metric.tcp_count += 1; + } + } + IpProto::Udp(udp_packet) => { + if port_range.contains(&udp_packet.dst_port) { + metric.udp_count += 1; + } + } + _ => {} + }, + IpPacket::V6(ipv6_packet) => match ipv6_packet.proto { + IpProto::Tcp(tcp_packet) => { + if port_range.contains(&tcp_packet.dst_port) { + metric.tcp_count += 1; + } + } + IpProto::Udp(udp_packet) => { + if port_range.contains(&udp_packet.dst_port) { + metric.udp_count += 1; + } + } + _ => {} + }, + } + } + } + } + + last_index = app_packets.len(); + + if terminate.load(std::sync::atomic::Ordering::Relaxed) { + break 'main; + } + } + } + }); + + self.metrics.push(port_count_metric); + if self.metrics.len() == 1 { + self.state.selected = Some(0); + } + + self.user_input.clear(); + } + + _ => { + self.user_input.input.handle_event(&Event::Key(key_event)); + } + } + + Ok(()) + } + + pub fn render_new_rule_popup(&self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(10), // Form + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let (form_block, message_block) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Fill(1), Constraint::Length(3)]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(block); + + (chunks[0], chunks[1]) + }; + + //TODO: Center + let rows = [ + Row::new(vec![ + Cell::from("Port Packet Counter".to_string()) + .bg(Color::DarkGray) + .fg(Color::White), + Cell::from(self.user_input.input.value()) + .bg(Color::DarkGray) + .fg(Color::White), + ]), + Row::new(vec![Cell::new(""), Cell::new("")]), + Row::new(vec![ + Cell::new(""), + Cell::from({ + if let Some(error) = &self.user_input.error { + error.to_string() + } else { + String::new() + } + }) + .red(), + ]), + ]; + + let widths = [Constraint::Percentage(49), Constraint::Percentage(49)]; + + let table = Table::new(rows, widths) + .header( + Row::new(vec![ + Line::from("Metric").centered(), + Line::from("From").centered(), + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .highlight_spacing(HighlightSpacing::Always); + + let help_message = + Text::styled("💡Examples: 443, 8080, 5555-9999", Style::new().dark_gray()).centered(); + + frame.render_widget(Clear, block); + frame.render_widget( + table, + form_block.inner(Margin { + horizontal: 2, + vertical: 0, + }), + ); + frame.render_widget( + help_message, + message_block.inner(Margin { + horizontal: 2, + vertical: 0, + }), + ); + + frame.render_widget( + Block::default() + .title(" Metrics Explorer ") + .bold() + .title_alignment(ratatui::layout::Alignment::Center) + .borders(Borders::all()) + .border_type(ratatui::widgets::BorderType::Thick) + .border_style(Style::default().green()) + .padding(Padding::uniform(1)), + block, + ); + } +} diff --git a/oryx-tui/src/ui.rs b/oryx-tui/src/ui.rs index 1453a73..864b79a 100644 --- a/oryx-tui/src/ui.rs +++ b/oryx-tui/src/ui.rs @@ -11,6 +11,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { ActivePopup::PacketInfos => app.section.inspection.render_packet_infos_popup(frame), ActivePopup::UpdateFilters => app.filter.render_update_popup(frame), ActivePopup::NewFirewallRule => app.section.firewall.render_new_rule_popup(frame), + ActivePopup::NewMetricExplorer => app.section.metrics.render_new_rule_popup(frame), } } for (index, notification) in app.notifications.iter().enumerate() {