diff --git a/src/gui/settings/mod.rs b/src/gui/settings/mod.rs index 4582f9c..1d84d1c 100644 --- a/src/gui/settings/mod.rs +++ b/src/gui/settings/mod.rs @@ -73,7 +73,8 @@ async fn fetch_admin_keys(host: String, flow: String, basic_auth: String) -> Res info!("Fetching admin keys from: {}", url); let client_builder = Client::builder() - .timeout(std::time::Duration::from_secs(30)); + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()); // Don't follow redirects let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -92,6 +93,16 @@ async fn fetch_admin_keys(host: String, flow: String, basic_auth: String) -> Res let response = request.send().await .map_err(|e| format!("Request failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -99,6 +110,11 @@ async fn fetch_admin_keys(host: String, flow: String, basic_auth: String) -> Res let body = response.text().await .map_err(|e| format!("Failed to read response: {}", e))?; + // Check if response looks like HTML (login page) + if body.trim_start().starts_with(" = serde_json::from_str(&body) .map_err(|e| format!("Failed to parse response: {}", e))?; @@ -106,12 +122,26 @@ async fn fetch_admin_keys(host: String, flow: String, basic_auth: String) -> Res Ok(keys) } +fn get_default_known_hosts_path() -> String { + #[cfg(target_os = "windows")] + { + if let Ok(user_profile) = std::env::var("USERPROFILE") { + format!("{}/.ssh/known_hosts", user_profile) + } else { + "~/.ssh/known_hosts".to_string() + } + } + #[cfg(not(target_os = "windows"))] + { + "~/.ssh/known_hosts".to_string() + } +} impl Default for KhmSettings { fn default() -> Self { Self { host: String::new(), flow: String::new(), - known_hosts: "~/.ssh/known_hosts".to_string(), + known_hosts: get_default_known_hosts_path(), basic_auth: String::new(), in_place: true, auto_sync_interval_minutes: 60, @@ -130,10 +160,19 @@ pub fn get_config_path() -> PathBuf { pub fn load_settings() -> KhmSettings { let path = get_config_path(); match fs::read_to_string(&path) { - Ok(contents) => serde_json::from_str(&contents).unwrap_or_else(|e| { - error!("Failed to parse KHM config: {}", e); - KhmSettings::default() - }), + Ok(contents) => { + let mut settings: KhmSettings = serde_json::from_str(&contents).unwrap_or_else(|e| { + error!("Failed to parse KHM config: {}", e); + KhmSettings::default() + }); + + // Fill in default known_hosts path if empty + if settings.known_hosts.is_empty() { + settings.known_hosts = get_default_known_hosts_path(); + } + + settings + } Err(_) => { debug!("KHM config file not found, using defaults"); KhmSettings::default() @@ -169,6 +208,9 @@ struct KhmSettingsWindow { admin_receiver: Option, String>>>, operation_receiver: Option>>, current_tab: SettingsTab, + is_syncing: bool, + sync_result_receiver: Option>>, + sync_status: SyncStatus, } #[derive(Debug, Clone, PartialEq)] @@ -184,6 +226,13 @@ enum ConnectionStatus { Error(String), } +#[derive(Debug, Clone)] +enum SyncStatus { + Unknown, + Success { keys_count: usize }, + Error(String), +} + impl eframe::App for KhmSettingsWindow { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // Check for test connection result @@ -234,6 +283,59 @@ impl eframe::App for KhmSettingsWindow { } } + // Check for sync result + if let Some(receiver) = &self.sync_result_receiver { + if let Ok(result) = receiver.try_recv() { + self.is_syncing = false; + match result { + Ok(message) => { + info!("Parsing sync result message: '{}'", message); + + // Parse keys count from message - fix parsing patterns + let keys_count = if let Some(start) = message.find("updated with ") { + let search_start = start + "updated with ".len(); + if let Some(end) = message[search_start..].find(" keys") { + let number_str = &message[search_start..search_start + end]; + info!("Found 'updated with' pattern, parsing: '{}'", number_str); + number_str.parse::().unwrap_or(0) + } else { 0 } + } else if let Some(start) = message.find("Retrieved ") { + let search_start = start + "Retrieved ".len(); + if let Some(end) = message[search_start..].find(" keys") { + let number_str = &message[search_start..search_start + end]; + info!("Found 'Retrieved' pattern, parsing: '{}'", number_str); + number_str.parse::().unwrap_or(0) + } else { 0 } + } else { + // Try to extract any number followed by "keys" in the message + if let Some(keys_pos) = message.find(" keys") { + let before_keys = &message[..keys_pos]; + // Find the last number in the string before "keys" + if let Some(space_pos) = before_keys.rfind(' ') { + let number_str = &before_keys[space_pos + 1..]; + info!("Found fallback pattern, parsing: '{}'", number_str); + number_str.parse::().unwrap_or(0) + } else { + 0 + } + } else { + 0 + } + }; + + info!("Parsed keys count: {}", keys_count); + self.sync_status = SyncStatus::Success { keys_count }; + info!("Sync successful: {}", message); + } + Err(error) => { + self.sync_status = SyncStatus::Error(error.clone()); + error!("Sync failed: {}", error); + } + } + self.sync_result_receiver = None; + ctx.request_repaint(); + } + } // Check for operation results if let Some(receiver) = &self.operation_receiver { if let Ok(result) = receiver.try_recv() { @@ -299,221 +401,266 @@ impl eframe::App for KhmSettingsWindow { impl KhmSettingsWindow { fn render_connection_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { - // Connection section - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new("🌐 Connection").size(16.0).strong()); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let mut connected = matches!(self.connection_status, ConnectionStatus::Connected { .. }); - ui.add_enabled(false, egui::Checkbox::new(&mut connected, "Connected")); + let available_height = ui.available_height(); + let button_area_height = 120.0; // Reserve space for buttons and status + let content_height = available_height - button_area_height; + + // Main content area (scrollable) + ui.allocate_ui_with_layout( + [ui.available_width(), content_height].into(), + egui::Layout::top_down(egui::Align::Min), + |ui| { + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + // Connection section + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("🌐 Connection").size(16.0).strong()); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut connected = matches!(self.connection_status, ConnectionStatus::Connected { .. }); + ui.add_enabled(false, egui::Checkbox::new(&mut connected, "Connected")); + + if self.is_testing_connection { + ui.spinner(); + ui.label(egui::RichText::new("Testing...").italics()); + } else { + match &self.connection_status { + ConnectionStatus::Unknown => { + ui.label(egui::RichText::new("Not tested").color(egui::Color32::GRAY)); + } + ConnectionStatus::Connected { keys_count, flow } => { + ui.label(egui::RichText::new("✅").color(egui::Color32::GREEN)); + ui.label(egui::RichText::new(format!("{} keys in '{}'", keys_count, flow)) + .color(egui::Color32::LIGHT_GREEN)); + } + ConnectionStatus::Error(err) => { + ui.label(egui::RichText::new("❌").color(egui::Color32::RED)) + .on_hover_text(format!("Error: {}", err)); + ui.label(egui::RichText::new("Failed").color(egui::Color32::RED)); + } + } + } + }); + }); + + ui.add_space(5.0); + + egui::Grid::new("connection_grid") + .num_columns(2) + .min_col_width(120.0) + .spacing([10.0, 8.0]) + .show(ui, |ui| { + ui.label("Host URL:"); + ui.add_sized( + [ui.available_width(), 20.0], + egui::TextEdit::singleline(&mut self.settings.host) + .hint_text("https://your-khm-server.com") + ); + ui.end_row(); + + ui.label("Flow Name:"); + ui.add_sized( + [ui.available_width(), 20.0], + egui::TextEdit::singleline(&mut self.settings.flow) + .hint_text("production, staging, etc.") + ); + ui.end_row(); + + ui.label("Basic Auth:"); + ui.add_sized( + [ui.available_width(), 20.0], + egui::TextEdit::singleline(&mut self.settings.basic_auth) + .hint_text("username:password (optional)") + .password(true) + ); + ui.end_row(); + }); + }); + }); - if self.is_testing_connection { - ui.spinner(); - ui.label(egui::RichText::new("Testing...").italics()); + ui.add_space(10.0); + + // Local settings section + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("📁 Local Settings").size(16.0).strong()); + ui.add_space(8.0); + + egui::Grid::new("local_grid") + .num_columns(2) + .min_col_width(120.0) + .spacing([10.0, 8.0]) + .show(ui, |ui| { + ui.label("Known Hosts File:"); + ui.add_sized( + [ui.available_width(), 20.0], + egui::TextEdit::singleline(&mut self.settings.known_hosts) + .hint_text("~/.ssh/known_hosts") + ); + ui.end_row(); + }); + + ui.add_space(8.0); + ui.checkbox(&mut self.settings.in_place, "✏ Update known_hosts file in-place after sync"); + }); + }); + + ui.add_space(15.0); + + // Auto-sync section + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("🔄 Auto Sync").size(16.0).strong()); + ui.add_space(8.0); + + let is_auto_sync_enabled = !self.settings.host.is_empty() + && !self.settings.flow.is_empty() + && self.settings.in_place; + + ui.horizontal(|ui| { + ui.label("Interval (minutes):"); + ui.add_sized( + [80.0, 20.0], + egui::TextEdit::singleline(&mut self.auto_sync_interval_str) + ); + + if let Ok(value) = self.auto_sync_interval_str.parse::() { + if value > 0 { + self.settings.auto_sync_interval_minutes = value; + } + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if is_auto_sync_enabled { + ui.label(egui::RichText::new("🔄 Enabled").color(egui::Color32::GREEN)); + } else { + ui.label(egui::RichText::new("❌ Disabled").color(egui::Color32::YELLOW)); + ui.label("(Configure host, flow & enable in-place sync)"); + } + }); + }); + }); + }); + + ui.add_space(10.0); + + // Advanced settings (collapsible) + ui.collapsing("🔧 Advanced Settings", |ui| { + ui.indent("advanced", |ui| { + ui.label("Configuration details:"); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Config file:"); + let config_path = get_config_path(); + ui.label(egui::RichText::new(config_path.display().to_string()) + .font(egui::FontId::monospace(12.0)) + .color(egui::Color32::LIGHT_GRAY)); + }); + + ui.add_space(8.0); + ui.label("Current configuration:"); + + ui.add_sized( + [ui.available_width(), 120.0], + egui::TextEdit::multiline(&mut self.config_content.clone()) + .font(egui::FontId::monospace(11.0)) + .interactive(false) + ); + }); + }); + }); + }, + ); + + // Bottom area with buttons (fixed position) + ui.allocate_ui_with_layout( + [ui.available_width(), button_area_height].into(), + egui::Layout::bottom_up(egui::Align::Min), + |ui| { + // Show sync status + match &self.sync_status { + SyncStatus::Success { keys_count } => { + ui.label(egui::RichText::new(format!("✅ Last sync successful: {} keys", keys_count)) + .color(egui::Color32::GREEN)); + } + SyncStatus::Error(err) => { + ui.label(egui::RichText::new(format!("❌ Sync failed: {}", err)) + .color(egui::Color32::RED)); + } + SyncStatus::Unknown => {} + } + + // Show validation hints + let save_enabled = !self.settings.host.is_empty() && !self.settings.flow.is_empty(); + if !save_enabled { + ui.label(egui::RichText::new("❗ Please fill in Host URL and Flow Name to save settings") + .color(egui::Color32::YELLOW) + .italics()); + } + + ui.add_space(5.0); + + // Action buttons + ui.horizontal(|ui| { + if ui.add_enabled( + save_enabled, + egui::Button::new("💾 Save Settings") + .min_size(egui::vec2(120.0, 32.0)) + ).clicked() { + if let Err(e) = save_settings(&self.settings) { + error!("Failed to save KHM settings: {}", e); } else { - match &self.connection_status { - ConnectionStatus::Unknown => { - ui.label(egui::RichText::new("Not tested").color(egui::Color32::GRAY)); - } - ConnectionStatus::Connected { keys_count, flow } => { - ui.label(egui::RichText::new("✅").color(egui::Color32::GREEN)); - ui.label(egui::RichText::new(format!("{} keys in '{}'", keys_count, flow)) - .color(egui::Color32::LIGHT_GREEN)); - } - ConnectionStatus::Error(err) => { - ui.label(egui::RichText::new("✗").color(egui::Color32::RED)) - .on_hover_text(format!("Error: {}", err)); - ui.label(egui::RichText::new("Failed").color(egui::Color32::RED)); - } - } + info!("KHM settings saved successfully"); } - }); - }); - - ui.add_space(8.0); - - egui::Grid::new("connection_grid") - .num_columns(2) - .min_col_width(120.0) - .spacing([10.0, 8.0]) - .show(ui, |ui| { - ui.label("Host URL:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut self.settings.host) - .hint_text("https://your-khm-server.com") - ); - ui.end_row(); - - ui.label("Flow Name:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut self.settings.flow) - .hint_text("production, staging, etc.") - ); - ui.end_row(); - - ui.label("Basic Auth:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut self.settings.basic_auth) - .hint_text("username:password (optional)") - .password(true) - ); - ui.end_row(); - }); - }); - }); - - ui.add_space(15.0); - - // Local settings section - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.label(egui::RichText::new("📁 Local Settings").size(16.0).strong()); - ui.add_space(8.0); - - egui::Grid::new("local_grid") - .num_columns(2) - .min_col_width(120.0) - .spacing([10.0, 8.0]) - .show(ui, |ui| { - ui.label("Known Hosts File:"); - ui.add_sized( - [ui.available_width(), 20.0], - egui::TextEdit::singleline(&mut self.settings.known_hosts) - .hint_text("~/.ssh/known_hosts") - ); - ui.end_row(); - }); - - ui.add_space(8.0); - ui.checkbox(&mut self.settings.in_place, "✏ Update known_hosts file in-place after sync"); - }); - }); - - ui.add_space(15.0); - - // Auto-sync section - ui.group(|ui| { - ui.set_min_width(ui.available_width()); - ui.vertical(|ui| { - ui.label(egui::RichText::new("🔄 Auto Sync").size(16.0).strong()); - ui.add_space(8.0); - - let is_auto_sync_enabled = !self.settings.host.is_empty() - && !self.settings.flow.is_empty() - && self.settings.in_place; - - ui.horizontal(|ui| { - ui.label("Interval (minutes):"); - ui.add_sized( - [80.0, 20.0], - egui::TextEdit::singleline(&mut self.auto_sync_interval_str) - ); + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } - if let Ok(value) = self.auto_sync_interval_str.parse::() { - if value > 0 { - self.settings.auto_sync_interval_minutes = value; - } + if ui.add( + egui::Button::new("✖ Cancel") + .min_size(egui::vec2(80.0, 32.0)) + ).clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if is_auto_sync_enabled { - ui.label(egui::RichText::new("🔄 Enabled").color(egui::Color32::GREEN)); - } else { - ui.label(egui::RichText::new("❌ Disabled").color(egui::Color32::YELLOW)); - ui.label("(Configure host, flow & enable in-place sync)"); + let can_test = !self.settings.host.is_empty() && !self.settings.flow.is_empty() && !self.is_testing_connection; + let can_sync = !self.settings.host.is_empty() && !self.settings.flow.is_empty() && !self.is_syncing; + + if ui.add_enabled( + can_test, + egui::Button::new( + if self.is_testing_connection { + "▶ Testing..." + } else { + "🔍 Test Connection" + } + ).min_size(egui::vec2(120.0, 32.0)) + ).clicked() { + self.start_connection_test(ctx); + } + + if ui.add_enabled( + can_sync, + egui::Button::new( + if self.is_syncing { + "🔄 Syncing..." + } else { + "🔄 Sync Now" + } + ).min_size(egui::vec2(100.0, 32.0)) + ).clicked() { + self.start_manual_sync(ctx); } }); }); - }); - }); - - ui.add_space(15.0); - - // Advanced settings (collapsible) - ui.collapsing("🔧 Advanced Settings", |ui| { - ui.indent("advanced", |ui| { - ui.label("Configuration details:"); - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.label("Config file:"); - let config_path = get_config_path(); - ui.label(egui::RichText::new(config_path.display().to_string()) - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::LIGHT_GRAY)); - }); - - ui.add_space(8.0); - ui.label("Current configuration:"); - - ui.add_sized( - [ui.available_width(), 120.0], - egui::TextEdit::multiline(&mut self.config_content.clone()) - .font(egui::FontId::monospace(11.0)) - .interactive(false) - ); - }); - }); - - ui.add_space(20.0); - ui.separator(); - ui.add_space(15.0); - - // Action buttons - let save_enabled = !self.settings.host.is_empty() && !self.settings.flow.is_empty(); - - ui.horizontal(|ui| { - if ui.add_enabled( - save_enabled, - egui::Button::new("💾 Save Settings") - .min_size(egui::vec2(120.0, 32.0)) - ).clicked() { - if let Err(e) = save_settings(&self.settings) { - error!("Failed to save KHM settings: {}", e); - } else { - info!("KHM settings saved successfully"); - } - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - - if ui.add( - egui::Button::new("✖ Cancel") - .min_size(egui::vec2(80.0, 32.0)) - ).clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let can_test = !self.settings.host.is_empty() && !self.settings.flow.is_empty() && !self.is_testing_connection; - - if ui.add_enabled( - can_test, - egui::Button::new( - if self.is_testing_connection { - "▶ Testing..." - } else { - "🔍 Test Connection" - } - ).min_size(egui::vec2(120.0, 32.0)) - ).clicked() { - self.start_connection_test(ctx); - } - }); - }); - - // Show validation hints - if !save_enabled { - ui.add_space(5.0); - ui.label(egui::RichText::new("❗ Please fill in Host URL and Flow Name to save settings") - .color(egui::Color32::YELLOW) - .italics()); - } + }, + ); } fn start_connection_test(&mut self, ctx: &egui::Context) { @@ -543,6 +690,31 @@ impl KhmSettingsWindow { }); } + fn start_manual_sync(&mut self, ctx: &egui::Context) { + if self.is_syncing { + return; + } + + self.is_syncing = true; + self.sync_status = SyncStatus::Unknown; + + let (tx, rx) = mpsc::channel(); + self.sync_result_receiver = Some(rx); + + let settings = self.settings.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + perform_manual_sync(settings).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + fn render_admin_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { // Admin tab header ui.horizontal(|ui| { @@ -866,7 +1038,7 @@ impl KhmSettingsWindow { } // Modern expand/collapse button - let expand_icon = if is_expanded { "🔺" } else { "🔻" }; + let expand_icon = if is_expanded { "▼" } else { "▶" }; if ui.add(egui::Button::new(expand_icon) .fill(egui::Color32::TRANSPARENT) .stroke(egui::Stroke::NONE) @@ -1257,6 +1429,9 @@ pub fn run_settings_window() { admin_receiver: None, operation_receiver: None, current_tab: SettingsTab::Connection, + is_syncing: false, + sync_result_receiver: None, + sync_status: SyncStatus::Unknown, }))), ); } @@ -1293,7 +1468,8 @@ async fn bulk_deprecate_servers(host: String, flow: String, basic_auth: String, info!("Bulk deprecating {} servers at: {}", servers.len(), url); let client_builder = Client::builder() - .timeout(std::time::Duration::from_secs(30)); + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()); let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -1315,6 +1491,16 @@ async fn bulk_deprecate_servers(host: String, flow: String, basic_auth: String, let response = request.send().await .map_err(|e| format!("Request failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -1343,7 +1529,8 @@ async fn bulk_restore_servers(host: String, flow: String, basic_auth: String, se info!("Bulk restoring {} servers at: {}", servers.len(), url); let client_builder = Client::builder() - .timeout(std::time::Duration::from_secs(30)); + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()); let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -1365,6 +1552,16 @@ async fn bulk_restore_servers(host: String, flow: String, basic_auth: String, se let response = request.send().await .map_err(|e| format!("Request failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -1393,7 +1590,8 @@ async fn deprecate_key_by_server(host: String, flow: String, basic_auth: String, info!("Deprecating key for server '{}' at: {}", server, url); let client_builder = Client::builder() - .timeout(std::time::Duration::from_secs(30)); + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()); let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -1412,6 +1610,16 @@ async fn deprecate_key_by_server(host: String, flow: String, basic_auth: String, let response = request.send().await .map_err(|e| format!("Request failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -1440,7 +1648,8 @@ async fn restore_key_by_server(host: String, flow: String, basic_auth: String, s info!("Restoring key for server '{}' at: {}", server, url); let client_builder = Client::builder() - .timeout(std::time::Duration::from_secs(30)); + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()); let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -1459,6 +1668,16 @@ async fn restore_key_by_server(host: String, flow: String, basic_auth: String, s let response = request.send().await .map_err(|e| format!("Request failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -1487,7 +1706,8 @@ async fn permanently_delete_key_by_server(host: String, flow: String, basic_auth info!("Permanently deleting key for server '{}' at: {}", server, url); let client_builder = Client::builder() - .timeout(std::time::Duration::from_secs(30)); + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()); let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -1506,6 +1726,16 @@ async fn permanently_delete_key_by_server(host: String, flow: String, basic_auth let response = request.send().await .map_err(|e| format!("Request failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -1534,7 +1764,8 @@ async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> info!("Testing connection to: {}", url); let client_builder = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(10)); + .timeout(std::time::Duration::from_secs(10)) + .redirect(reqwest::redirect::Policy::none()); // Don't follow redirects let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; @@ -1553,6 +1784,16 @@ async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> let response = request.send().await .map_err(|e| format!("Connection failed: {}", e))?; + // Check for authentication required + if response.status().as_u16() == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + // Check for redirects (usually to login page) + if response.status().as_u16() >= 300 && response.status().as_u16() < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + if !response.status().is_success() { return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); } @@ -1560,7 +1801,6 @@ async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> let body = response.text().await .map_err(|e| format!("Failed to read response: {}", e))?; - // Parse JSON response to count SSH keys if body.trim().is_empty() { return Err("Server returned empty response".to_string()); } @@ -1571,19 +1811,21 @@ async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> match keys { Ok(key_array) => { let ssh_key_count = key_array.len(); - if ssh_key_count == 0 { - return Err("No SSH keys found in response".to_string()); - } Ok(format!("Connection successful! Found {} SSH keys in flow '{}'", ssh_key_count, flow)) } Err(_) => { + // Check if response looks like HTML (login page) + if body.trim_start().starts_with(" = body.lines().collect(); let ssh_key_count = lines.iter() .filter(|line| !line.trim().is_empty() && !line.starts_with('#')) .count(); - if ssh_key_count == 0 { + if ssh_key_count == 0 && !body.trim().is_empty() { return Err("Invalid response format - not JSON array or SSH keys text".to_string()); } @@ -1591,3 +1833,65 @@ async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> } } } + +async fn perform_manual_sync(settings: KhmSettings) -> Result { + use crate::Args; + + if settings.host.is_empty() || settings.flow.is_empty() { + return Err("Host and flow must be configured".to_string()); + } + + if settings.known_hosts.is_empty() { + return Err("Known hosts file path must be configured".to_string()); + } + + info!("Starting manual sync with host: {}, flow: {}", settings.host, settings.flow); + + // Convert KhmSettings to Args for client module + let args = Args { + server: false, + gui: false, + settings_ui: false, + in_place: settings.in_place, + flows: vec!["default".to_string()], // Not used in client mode + ip: "127.0.0.1".to_string(), // Not used in client mode + port: 8080, // Not used in client mode + db_host: "127.0.0.1".to_string(), // Not used in client mode + db_name: "khm".to_string(), // Not used in client mode + db_user: None, // Not used in client mode + db_password: None, // Not used in client mode + host: Some(settings.host.clone()), + flow: Some(settings.flow.clone()), + known_hosts: expand_path(&settings.known_hosts), + basic_auth: settings.basic_auth.clone(), + }; + + // Get keys count before sync + let keys_before = crate::client::read_known_hosts(&args.known_hosts) + .unwrap_or_else(|_| Vec::new()) + .len(); + + // Perform sync + crate::client::run_client(args.clone()).await + .map_err(|e| format!("Sync failed: {}", e))?; + + // Get keys count after sync + let keys_after = if args.in_place { + crate::client::read_known_hosts(&args.known_hosts) + .unwrap_or_else(|_| Vec::new()) + .len() + } else { + keys_before + }; + + info!("Manual sync completed: {} keys before, {} keys after", keys_before, keys_after); + + let result_message = if args.in_place { + format!("Sync completed successfully! Known hosts file updated with {} keys (was {})", keys_after, keys_before) + } else { + format!("Sync completed successfully! Retrieved {} keys from server", keys_after) + }; + + info!("Returning sync result message: '{}'", result_message); + Ok(result_message) +} diff --git a/src/main.rs b/src/main.rs index 2f36397..067a262 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,26 +30,26 @@ use log::{error, info}; pub struct Args { /// Run in server mode (default: false) #[arg(long, help = "Run in server mode")] - server: bool, + pub server: bool, /// Run with GUI tray interface (default: false) #[arg(long, help = "Run with GUI tray interface")] - gui: bool, + pub gui: bool, /// Run settings UI window (used with --gui) #[arg(long, help = "Run settings UI window (used with --gui)")] - settings_ui: bool, + pub settings_ui: bool, /// Update the known_hosts file with keys from the server after sending keys (default: false) #[arg( long, help = "Server mode: Sync the known_hosts file with keys from the server" )] - in_place: bool, + pub in_place: bool, /// Comma-separated list of flows to manage (default: default) #[arg(long, default_value = "default", value_parser, num_args = 1.., value_delimiter = ',', help = "Server mode: Comma-separated list of flows to manage")] - flows: Vec, + pub flows: Vec, /// IP address to bind the server or client to (default: 127.0.0.1) #[arg( @@ -58,7 +58,7 @@ pub struct Args { default_value = "127.0.0.1", help = "Server mode: IP address to bind the server to" )] - ip: String, + pub ip: String, /// Port to bind the server or client to (default: 8080) #[arg( @@ -67,7 +67,7 @@ pub struct Args { default_value = "8080", help = "Server mode: Port to bind the server to" )] - port: u16, + pub port: u16, /// Hostname or IP address of the PostgreSQL database (default: 127.0.0.1) #[arg( @@ -75,7 +75,7 @@ pub struct Args { default_value = "127.0.0.1", help = "Server mode: Hostname or IP address of the PostgreSQL database" )] - db_host: String, + pub db_host: String, /// Name of the PostgreSQL database (default: khm) #[arg( @@ -83,7 +83,7 @@ pub struct Args { default_value = "khm", help = "Server mode: Name of the PostgreSQL database" )] - db_name: String, + pub db_name: String, /// Username for the PostgreSQL database (required in server mode) #[arg( @@ -91,7 +91,7 @@ pub struct Args { required_if_eq("server", "true"), help = "Server mode: Username for the PostgreSQL database" )] - db_user: Option, + pub db_user: Option, /// Password for the PostgreSQL database (required in server mode) #[arg( @@ -99,7 +99,7 @@ pub struct Args { required_if_eq("server", "true"), help = "Server mode: Password for the PostgreSQL database" )] - db_password: Option, + pub db_password: Option, /// Host address of the server to connect to in client mode (required in client mode) #[arg( @@ -107,7 +107,7 @@ pub struct Args { required_if_eq("server", "false"), help = "Client mode: Full host address of the server to connect to. Like https://khm.example.com" )] - host: Option, + pub host: Option, /// Flow name to use on the server #[arg( @@ -115,7 +115,7 @@ pub struct Args { required_if_eq("server", "false"), help = "Client mode: Flow name to use on the server" )] - flow: Option, + pub flow: Option, /// Path to the known_hosts file (default: ~/.ssh/known_hosts) #[arg( @@ -123,11 +123,11 @@ pub struct Args { default_value = "~/.ssh/known_hosts", help = "Client mode: Path to the known_hosts file" )] - known_hosts: String, + pub known_hosts: String, /// Basic auth string for client mode. Format: user:pass #[arg(long, default_value = "", help = "Client mode: Basic Auth credentials")] - basic_auth: String, + pub basic_auth: String, } #[actix_web::main]