mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-21 14:27:14 +00:00
399 lines
12 KiB
Rust
399 lines
12 KiB
Rust
use super::state::{SshKey, DnsResult, AdminSettings};
|
|
use log::info;
|
|
use reqwest::Client;
|
|
use std::time::Duration;
|
|
|
|
/// Create HTTP client for API requests
|
|
fn create_http_client() -> Result<Client, String> {
|
|
Client::builder()
|
|
.timeout(Duration::from_secs(30))
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
.build()
|
|
.map_err(|e| format!("Failed to create HTTP client: {}", e))
|
|
}
|
|
|
|
/// Add basic auth to request if provided
|
|
fn add_auth_if_needed(
|
|
request: reqwest::RequestBuilder,
|
|
basic_auth: &str,
|
|
) -> Result<reqwest::RequestBuilder, String> {
|
|
if basic_auth.is_empty() {
|
|
return Ok(request);
|
|
}
|
|
|
|
let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect();
|
|
if auth_parts.len() == 2 {
|
|
Ok(request.basic_auth(auth_parts[0], Some(auth_parts[1])))
|
|
} else {
|
|
Err("Basic auth format should be 'username:password'".to_string())
|
|
}
|
|
}
|
|
|
|
/// Check response status for errors
|
|
fn check_response_status(response: &reqwest::Response) -> Result<(), String> {
|
|
let status = response.status().as_u16();
|
|
|
|
if status == 401 {
|
|
return Err("Authentication required. Please provide valid basic auth credentials.".to_string());
|
|
}
|
|
|
|
if status >= 300 && status < 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: {} {}",
|
|
status,
|
|
response.status().canonical_reason().unwrap_or("Unknown")
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if response is HTML instead of JSON
|
|
fn check_html_response(body: &str) -> Result<(), String> {
|
|
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());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get application version from API
|
|
pub async fn get_version(settings: &AdminSettings) -> Result<String, String> {
|
|
if settings.server_url.is_empty() {
|
|
return Err("Server URL must be specified".to_string());
|
|
}
|
|
|
|
let url = format!("{}/api/version", settings.server_url.trim_end_matches('/'));
|
|
info!("Getting version from: {}", url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.get(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
let body = response
|
|
.text()
|
|
.await
|
|
.map_err(|e| format!("Failed to read response: {}", e))?;
|
|
|
|
check_html_response(&body)?;
|
|
|
|
let version_response: serde_json::Value = serde_json::from_str(&body)
|
|
.map_err(|e| format!("Failed to parse version: {}", e))?;
|
|
|
|
let version = version_response
|
|
.get("version")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
|
|
info!("KHM server version: {}", version);
|
|
Ok(version)
|
|
}
|
|
|
|
/// Test connection to KHM server using existing API endpoint
|
|
pub async fn test_connection(settings: &AdminSettings) -> Result<String, String> {
|
|
if settings.server_url.is_empty() || settings.selected_flow.is_empty() {
|
|
return Err("Server URL and flow must be specified".to_string());
|
|
}
|
|
|
|
let url = format!(
|
|
"{}/{}/keys",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow
|
|
);
|
|
info!("Testing connection to: {}", url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.get(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
let body = response
|
|
.text()
|
|
.await
|
|
.map_err(|e| format!("Failed to read response: {}", e))?;
|
|
|
|
check_html_response(&body)?;
|
|
|
|
let keys: Vec<SshKey> = serde_json::from_str(&body)
|
|
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
|
|
|
let message = format!("Connection successful! Found {} SSH keys from flow '{}'", keys.len(), settings.selected_flow);
|
|
info!("{}", message);
|
|
Ok(message)
|
|
}
|
|
|
|
/// Load available flows from server
|
|
pub async fn load_flows(settings: &AdminSettings) -> Result<Vec<String>, String> {
|
|
if settings.server_url.is_empty() {
|
|
return Err("Server URL must be specified".to_string());
|
|
}
|
|
|
|
let url = format!("{}/api/flows", settings.server_url.trim_end_matches('/'));
|
|
info!("Loading flows from: {}", url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.get(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
let body = response
|
|
.text()
|
|
.await
|
|
.map_err(|e| format!("Failed to read response: {}", e))?;
|
|
|
|
check_html_response(&body)?;
|
|
|
|
let flows: Vec<String> = serde_json::from_str(&body)
|
|
.map_err(|e| format!("Failed to parse flows: {}", e))?;
|
|
|
|
info!("Loaded {} flows", flows.len());
|
|
Ok(flows)
|
|
}
|
|
|
|
/// Fetch all SSH keys including deprecated ones using existing API endpoint
|
|
pub async fn fetch_keys(settings: &AdminSettings) -> Result<Vec<SshKey>, String> {
|
|
if settings.server_url.is_empty() || settings.selected_flow.is_empty() {
|
|
return Err("Server URL and flow must be specified".to_string());
|
|
}
|
|
|
|
let url = format!(
|
|
"{}/{}/keys",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow
|
|
);
|
|
info!("Fetching keys from: {}", url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.get(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
let body = response
|
|
.text()
|
|
.await
|
|
.map_err(|e| format!("Failed to read response: {}", e))?;
|
|
|
|
check_html_response(&body)?;
|
|
|
|
let keys: Vec<SshKey> = serde_json::from_str(&body)
|
|
.map_err(|e| format!("Failed to parse keys: {}", e))?;
|
|
|
|
info!("Fetched {} SSH keys", keys.len());
|
|
Ok(keys)
|
|
}
|
|
|
|
/// Deprecate a key for a specific server
|
|
pub async fn deprecate_key(
|
|
settings: &AdminSettings,
|
|
server: &str,
|
|
) -> Result<String, String> {
|
|
let url = format!(
|
|
"{}/{}/keys/{}",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow,
|
|
urlencoding::encode(server)
|
|
);
|
|
info!("Deprecating key for server '{}' at: {}", server, url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.delete(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
Ok(format!("Successfully deprecated key for server '{}'", server))
|
|
}
|
|
|
|
/// Restore a key for a specific server
|
|
pub async fn restore_key(
|
|
settings: &AdminSettings,
|
|
server: &str,
|
|
) -> Result<String, String> {
|
|
let url = format!(
|
|
"{}/{}/keys/{}/restore",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow,
|
|
urlencoding::encode(server)
|
|
);
|
|
info!("Restoring key for server '{}' at: {}", server, url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.post(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
Ok(format!("Successfully restored key for server '{}'", server))
|
|
}
|
|
|
|
/// Delete a key permanently for a specific server
|
|
pub async fn delete_key(
|
|
settings: &AdminSettings,
|
|
server: &str,
|
|
) -> Result<String, String> {
|
|
let url = format!(
|
|
"{}/{}/keys/{}/delete",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow,
|
|
urlencoding::encode(server)
|
|
);
|
|
info!("Permanently deleting key for server '{}' at: {}", server, url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.delete(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
Ok(format!("Successfully deleted key for server '{}'", server))
|
|
}
|
|
|
|
/// Bulk deprecate multiple servers
|
|
pub async fn bulk_deprecate_servers(
|
|
settings: &AdminSettings,
|
|
servers: Vec<String>,
|
|
) -> Result<String, String> {
|
|
let url = format!(
|
|
"{}/{}/bulk-deprecate",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow
|
|
);
|
|
info!("Bulk deprecating {} servers at: {}", servers.len(), url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.post(&url).json(&serde_json::json!({
|
|
"servers": servers
|
|
}));
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
Ok("Successfully deprecated selected servers".to_string())
|
|
}
|
|
|
|
/// Bulk restore multiple servers
|
|
pub async fn bulk_restore_servers(
|
|
settings: &AdminSettings,
|
|
servers: Vec<String>,
|
|
) -> Result<String, String> {
|
|
let url = format!(
|
|
"{}/{}/bulk-restore",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow
|
|
);
|
|
info!("Bulk restoring {} servers at: {}", servers.len(), url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.post(&url).json(&serde_json::json!({
|
|
"servers": servers
|
|
}));
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
Ok("Successfully restored selected servers".to_string())
|
|
}
|
|
|
|
/// Scan DNS resolution for servers using existing API endpoint
|
|
pub async fn scan_dns_resolution(
|
|
settings: &AdminSettings,
|
|
) -> Result<Vec<DnsResult>, String> {
|
|
let url = format!(
|
|
"{}/{}/scan-dns",
|
|
settings.server_url.trim_end_matches('/'),
|
|
settings.selected_flow
|
|
);
|
|
info!("Scanning DNS resolution at: {}", url);
|
|
|
|
let client = create_http_client()?;
|
|
let mut request = client.post(&url);
|
|
|
|
request = add_auth_if_needed(request, &settings.basic_auth)?;
|
|
|
|
let response = request
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
check_response_status(&response)?;
|
|
|
|
let body = response
|
|
.text()
|
|
.await
|
|
.map_err(|e| format!("Failed to read response: {}", e))?;
|
|
|
|
// Parse the response format from existing API: {"results": [...], "total": N, "unresolved": N}
|
|
let api_response: serde_json::Value = serde_json::from_str(&body)
|
|
.map_err(|e| format!("Failed to parse DNS response: {}", e))?;
|
|
|
|
let results = api_response
|
|
.get("results")
|
|
.and_then(|r| serde_json::from_value(r.clone()).ok())
|
|
.unwrap_or_else(Vec::new);
|
|
|
|
info!("DNS scan completed for {} servers", results.len());
|
|
Ok(results)
|
|
} |