use std::error::Error; use std::fs; use std::net::{Ipv4Addr, SocketAddrV4}; use serde::{Deserialize, Serialize}; use url::{Url}; use crate::api::APIErrorResponse; use crate::{HostCommands}; pub async fn host_main(command: HostCommands, server: Url) -> Result<(), Box> { match command { HostCommands::List {} => list_hosts(server).await, HostCommands::Create { name, network_id, role_id, ip_address, listen_port, lighthouse, relay, static_address } => create_host(name, network_id, role_id, ip_address, listen_port, lighthouse, relay, static_address, server).await, HostCommands::Lookup { id } => get_host(id, server).await, HostCommands::Delete { id } => delete_host(id, server).await, HostCommands::Update { id, listen_port, static_address, name, ip, role } => update_host(id, listen_port, static_address, name, ip, role, server).await, HostCommands::Block { id } => block_host(id, server).await, HostCommands::Enroll { id } => enroll_host(id, server).await } } #[derive(Deserialize)] pub struct HostListResp { pub data: Vec } #[derive(Serialize, Deserialize)] pub struct HostMetadata { #[serde(rename = "lastSeenAt")] pub last_seen_at: String, pub version: String, pub platform: String, #[serde(rename = "updateAvailable")] pub update_available: bool, } #[derive(Serialize, Deserialize)] pub struct Host { pub id: String, #[serde(rename = "organizationID")] pub organization_id: String, #[serde(rename = "networkID")] pub network_id: String, #[serde(rename = "roleID")] pub role_id: String, pub name: String, #[serde(rename = "ipAddress")] pub ip_address: String, #[serde(rename = "staticAddresses")] pub static_addresses: Vec, #[serde(rename = "listenPort")] pub listen_port: i64, #[serde(rename = "isLighthouse")] pub is_lighthouse: bool, #[serde(rename = "isRelay")] pub is_relay: bool, #[serde(rename = "createdAt")] pub created_at: String, #[serde(rename = "isBlocked")] pub is_blocked: bool, pub metadata: HostMetadata, } pub async fn list_hosts(server: Url) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.get(server.join("/v1/hosts")?).bearer_auth(token).send().await?; if res.status().is_success() { let resp: HostListResp = res.json().await?; for host in &resp.data { println!(" Host: {} ({})", host.name, host.id); println!(" Organization: {}", host.organization_id); println!(" Network: {}", host.network_id); println!(" Role: {}", host.role_id); println!(" IP Address: {}", host.ip_address); println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::>().join(", ")); println!(" Listen Port: {}", host.listen_port); println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } ); println!(" Blocked: {}", host.is_blocked); println!(" Last Seen: {}", host.metadata.last_seen_at); println!(" Client Version: {}", host.metadata.version); println!(" Platform: {}", host.metadata.platform); println!("Client Update Available: {}", host.metadata.update_available); println!(" Created: {}", host.created_at); println!(); } if resp.data.is_empty() { println!("No hosts found"); } } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error listing hosts: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } #[derive(Serialize, Deserialize)] pub struct HostCreateBody { pub name: String, #[serde(rename = "networkID")] pub network_id: String, #[serde(rename = "roleID")] pub role_id: String, #[serde(rename = "ipAddress")] pub ip_address: Ipv4Addr, #[serde(rename = "listenPort")] pub listen_port: u16, #[serde(rename = "isLighthouse")] pub is_lighthouse: bool, #[serde(rename = "isRelay")] pub is_relay: bool, #[serde(rename = "staticAddresses")] pub static_addresses: Vec, } #[derive(Serialize, Deserialize)] pub struct HostGetMetadata {} #[derive(Serialize, Deserialize)] pub struct HostGetResponse { pub data: Host, pub metadata: HostGetMetadata, } pub async fn create_host(name: String, network_id: String, role_id: String, ip_address: Ipv4Addr, listen_port: Option, lighthouse: bool, relay: bool, static_address: Option, server: Url) -> Result<(), Box> { if lighthouse && relay { eprintln!("[error] Error creating host: a host cannot be both a lighthouse and a relay at the same time"); std::process::exit(1); } if (lighthouse || relay) && static_address.is_none() { eprintln!("[error] Error creating host: a relay or lighthouse must have a static IP"); std::process::exit(1); } if (lighthouse || relay) && listen_port.is_none() { eprintln!("[error] Error creating host: a relay or lighthouse must have a listen port"); std::process::exit(1); } let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.post(server.join(&format!("/v1/hosts"))?).json(&HostCreateBody { name, network_id, role_id, ip_address, listen_port: listen_port.unwrap_or(0), is_lighthouse: lighthouse, is_relay: relay, static_addresses: static_address.map_or(vec![], |u| vec![u]), }).bearer_auth(token).send().await?; if res.status().is_success() { let host: Host = res.json::().await?.data; println!(" Host: {} ({})", host.name, host.id); println!(" Organization: {}", host.organization_id); println!(" Network: {}", host.network_id); println!(" Role: {}", host.role_id); println!(" IP Address: {}", host.ip_address); println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::>().join(", ")); println!(" Listen Port: {}", host.listen_port); println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } ); println!(" Blocked: {}", host.is_blocked); println!(" Last Seen: {}", host.metadata.last_seen_at); println!(" Client Version: {}", host.metadata.version); println!(" Platform: {}", host.metadata.platform); println!("Client Update Available: {}", host.metadata.update_available); println!(" Created: {}", host.created_at); println!(); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error creating host: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } pub async fn get_host(id: String, server: Url) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.get(server.join(&format!("/v1/hosts/{}", id))?).bearer_auth(token).send().await?; if res.status().is_success() { let host: Host = res.json::().await?.data; println!(" Host: {} ({})", host.name, host.id); println!(" Organization: {}", host.organization_id); println!(" Network: {}", host.network_id); println!(" Role: {}", host.role_id); println!(" IP Address: {}", host.ip_address); println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::>().join(", ")); println!(" Listen Port: {}", host.listen_port); println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } ); println!(" Blocked: {}", host.is_blocked); println!(" Last Seen: {}", host.metadata.last_seen_at); println!(" Client Version: {}", host.metadata.version); println!(" Platform: {}", host.metadata.platform); println!("Client Update Available: {}", host.metadata.update_available); println!(" Created: {}", host.created_at); println!(); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error listing hosts: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } pub async fn delete_host(id: String, server: Url) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.delete(server.join(&format!("/v1/hosts/{}", id))?).bearer_auth(token).send().await?; if res.status().is_success() { println!("Host removed"); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error removing host: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } #[derive(Serialize, Deserialize)] pub struct HostUpdateBody { #[serde(rename = "listenPort")] pub listen_port: u16, #[serde(rename = "staticAddresses")] pub static_addresses: Vec, pub name: Option, pub ip: Option, pub role: Option } pub async fn update_host(id: String, listen_port: Option, static_address: Option, name: Option, ip: Option, role: Option, server: Url) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.put(server.join(&format!("/v1/hosts/{}?extension=extended_hosts", id))?).json(&HostUpdateBody { listen_port: listen_port.unwrap_or(0), static_addresses: static_address.map_or_else(|| vec![], |u| vec![u]), name, ip, role }).bearer_auth(token).send().await?; if res.status().is_success() { let host: Host = res.json::().await?.data; println!(" Host: {} ({})", host.name, host.id); println!(" Organization: {}", host.organization_id); println!(" Network: {}", host.network_id); println!(" Role: {}", host.role_id); println!(" IP Address: {}", host.ip_address); println!(" Static Addresses: {}", host.static_addresses.iter().map(|u| u.to_string()).collect::>().join(", ")); println!(" Listen Port: {}", host.listen_port); println!(" Type: {}", if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" } ); println!(" Blocked: {}", host.is_blocked); println!(" Last Seen: {}", host.metadata.last_seen_at); println!(" Client Version: {}", host.metadata.version); println!(" Platform: {}", host.metadata.platform); println!("Client Update Available: {}", host.metadata.update_available); println!(" Created: {}", host.created_at); println!(); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error updating host: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } #[derive(Serialize, Deserialize)] pub struct EnrollmentCodeResponseMetadata {} #[derive(Serialize, Deserialize)] pub struct EnrollmentCode { pub code: String, #[serde(rename = "lifetimeSeconds")] pub lifetime_seconds: i64, } #[derive(Serialize, Deserialize)] pub struct EnrollmentResponseData { #[serde(rename = "enrollmentCode")] pub enrollment_code: EnrollmentCode, } #[derive(Serialize, Deserialize)] pub struct EnrollmentResponse { pub data: EnrollmentResponseData, pub metadata: EnrollmentCodeResponseMetadata, } pub async fn enroll_host(id: String, server: Url) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.post(server.join(&format!("/v1/hosts/{}/enrollment-code", id))?).bearer_auth(token).send().await?; if res.status().is_success() { let resp: EnrollmentResponse = res.json().await?; println!("Enrollment code generated. Enroll the host with the following code: {}", resp.data.enrollment_code.code); println!("This code will be valid for {} seconds, at which point you will need to generate a new code", resp.data.enrollment_code.lifetime_seconds); println!("If this host is blocked, a successful re-enrollment will unblock it."); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error blocking host: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } pub async fn block_host(id: String, server: Url) -> Result<(), Box> { let client = reqwest::Client::new(); // load session token let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token"); let session_token = fs::read_to_string(&sess_token_store)?; let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token"); let auth_token = fs::read_to_string(&auth_token_store)?; let token = format!("{} {}", session_token, auth_token); let res = client.post(server.join(&format!("/v1/hosts/{}/block", id))?).bearer_auth(token).send().await?; if res.status().is_success() { println!("Host blocked. To unblock it, re-enroll the host."); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error blocking host: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) }