Fix watcher

This commit is contained in:
Ultradesu
2025-07-22 17:27:19 +03:00
parent 6dc0f279b2
commit 6ad3cd8f23
2 changed files with 542 additions and 238 deletions

View File

@@ -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("<!DOCTYPE") || body.trim_start().starts_with("<html") {
return Err("Server returned HTML page instead of JSON. This usually means authentication is required or the endpoint is incorrect.".to_string());
}
let keys: Vec<SshKey> = 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<mpsc::Receiver<Result<Vec<SshKey>, String>>>,
operation_receiver: Option<mpsc::Receiver<Result<String, String>>>,
current_tab: SettingsTab,
is_syncing: bool,
sync_result_receiver: Option<mpsc::Receiver<Result<String, String>>>,
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::<usize>().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::<usize>().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::<usize>().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;
if self.is_testing_connection {
ui.spinner();
ui.label(egui::RichText::new("Testing...").italics());
// 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();
});
});
});
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::<u32>() {
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");
}
});
});
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
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)
);
if let Ok(value) = self.auto_sync_interval_str.parse::<u32>() {
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("<!DOCTYPE") || body.trim_start().starts_with("<html") {
return Err("Server returned HTML page instead of JSON. This usually means authentication is required or the endpoint is incorrect.".to_string());
}
// Fallback: try to parse as plain text (old format)
let lines: Vec<&str> = 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<String, String> {
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)
}

View File

@@ -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<String>,
pub flows: Vec<String>,
/// 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<String>,
pub db_user: Option<String>,
/// 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<String>,
pub db_password: Option<String>,
/// 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<String>,
pub host: Option<String>,
/// 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<String>,
pub flow: Option<String>,
/// 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]