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, HostOverrideCommands}; 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, HostCommands::Overrides { command } => match command { HostOverrideCommands::List { id } => list_overrides(id, server).await, HostOverrideCommands::Set { id, key, boolean, string, numeric } => set_override(id, key, boolean, numeric, string, server).await, HostOverrideCommands::Unset { id, key } => unset_override(id, key, 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?pageSize=5000")?).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, } #[allow(clippy::too_many_arguments)] 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("/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::new, |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))?).header("content-length", 0).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))?).header("Content-Length", "0").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(()) } #[derive(Serialize, Deserialize)] pub struct HostConfigOverrideResponse { pub data: HostConfigOverrideData } #[derive(Serialize, Deserialize)] pub struct HostConfigOverrideData { pub overrides: Vec } #[derive(Serialize, Deserialize)] pub struct HostConfigOverrideDataOverride { pub key: String, pub value: HostConfigOverrideDataOverrideValue } #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum HostConfigOverrideDataOverrideValue { Boolean(bool), Numeric(i64), Other(String) } pub async fn list_overrides(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/{}/config-overrides", id))?).bearer_auth(token).send().await?; if res.status().is_success() { let resp: HostConfigOverrideResponse = res.json().await?; for c_override in &resp.data.overrides { println!(" Key: {}", c_override.key); println!("Value: {}", match &c_override.value { HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v), HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v), HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v) }); } if resp.data.overrides.is_empty() { println!("No overrides found"); } } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error looking up config overrides: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } #[derive(Serialize, Deserialize)] pub struct SetOverrideRequest { pub overrides: Vec } pub async fn set_override(id: String, key: String, boolean: Option, numeric: Option, other: Option, server: Url) -> Result<(), Box> { if boolean.is_none() && numeric.is_none() && other.is_none() { eprintln!("[error] no value provided: you must provide at least --boolean, --numeric, or --string"); std::process::exit(1); } else if boolean.is_some() && numeric.is_some() || boolean.is_some() && other.is_some() || numeric.is_some() && other.is_some() { eprintln!("[error] multiple values provided: you must provide only one of --boolean, --numeric, or --string"); std::process::exit(1); } let val; if let Some(v) = boolean { val = HostConfigOverrideDataOverrideValue::Boolean(v); } else if let Some(v) = numeric { val = HostConfigOverrideDataOverrideValue::Numeric(v); } else if let Some(v) = other { val = HostConfigOverrideDataOverrideValue::Other(v); } else { unreachable!(); } 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/{}/config-overrides", id))?).bearer_auth(token.clone()).send().await?; if res.status().is_success() { let resp: HostConfigOverrideResponse = res.json().await?; let mut others: Vec = vec![]; for c_override in resp.data.overrides { if c_override.key != key { others.push(c_override); } } others.push(HostConfigOverrideDataOverride { key, value: val, }); let res = client.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).json(&SetOverrideRequest { overrides: others, }).send().await?; if res.status().is_success() { let resp: HostConfigOverrideResponse = res.json().await?; for c_override in &resp.data.overrides { println!(" Key: {}", c_override.key); println!("Value: {}", match &c_override.value { HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v), HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v), HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v) }); } if resp.data.overrides.is_empty() { println!("No overrides found"); } println!("Override set successfully"); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error setting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error setting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) } pub async fn unset_override(id: String, key: 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/{}/config-overrides", id))?).bearer_auth(token.clone()).send().await?; if res.status().is_success() { let resp: HostConfigOverrideResponse = res.json().await?; let mut others: Vec = vec![]; for c_override in resp.data.overrides { if c_override.key != key { others.push(c_override); } } let res = client.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).json(&SetOverrideRequest { overrides: others, }).send().await?; if res.status().is_success() { let resp: HostConfigOverrideResponse = res.json().await?; for c_override in &resp.data.overrides { println!(" Key: {}", c_override.key); println!("Value: {}", match &c_override.value { HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v), HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v), HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v) }); } if resp.data.overrides.is_empty() { println!("No overrides found"); } println!("Override unset successfully"); } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error unsetting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } } else { let resp: APIErrorResponse = res.json().await?; eprintln!("[error] Error unsetting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message); std::process::exit(1); } Ok(()) }