use std::collections::BTreeSet; use std::net::Ipv4Addr; use clap::Parser; use imbl_value::InternedString; use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::GatewayId; use crate::context::{CliContext, RpcContext}; use crate::db::model::DatabaseModel; use crate::net::acme::AcmeProvider; use crate::net::host::{HostApiKind, all_hosts}; use crate::prelude::*; use crate::util::serde::{HandlerExtSerde, display_serializable}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct HostAddress { pub address: InternedString, pub public: Option, pub private: bool, } #[derive(Debug, Clone, Deserialize, Serialize, TS)] pub struct PublicDomainConfig { pub gateway: GatewayId, pub acme: Option, } fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> { let mut domains = BTreeSet::::new(); let check_domain = |domains: &mut BTreeSet, domain: InternedString| { if domains.contains(&domain) { return Err(Error::new( eyre!("domain {domain} is already in use"), ErrorKind::InvalidRequest, )); } domains.insert(domain); Ok(()) }; let mut not_in_use = Vec::new(); for host in all_hosts(db) { let host = host?; let in_use = host.as_bindings().de()?.values().any(|v| v.enabled); if !in_use { not_in_use.push(host); continue; } let public = host.as_public_domains().keys()?; for domain in &public { check_domain(&mut domains, domain.clone())?; } for domain in host.as_private_domains().de()? { if !public.contains(&domain) { check_domain(&mut domains, domain)?; } } } for host in not_in_use { host.as_public_domains_mut() .mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?; host.as_private_domains_mut() .mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?; let public = host.as_public_domains().keys()?; for domain in &public { check_domain(&mut domains, domain.clone())?; } for domain in host.as_private_domains().de()? { if !public.contains(&domain) { check_domain(&mut domains, domain)?; } } } Ok(()) } pub fn address_api() -> ParentHandler { ParentHandler::::new() .subcommand( "domain", ParentHandler::::new() .subcommand( "public", ParentHandler::::new() .subcommand( "add", from_fn_async(add_public_domain::) .with_metadata("sync_db", Value::Bool(true)) .with_inherited(|_, a| a) .no_display() .with_about("about.add-public-domain-to-host") .with_call_remote::(), ) .subcommand( "remove", from_fn_async(remove_public_domain::) .with_metadata("sync_db", Value::Bool(true)) .with_inherited(|_, a| a) .no_display() .with_about("about.remove-public-domain-from-host") .with_call_remote::(), ) .with_inherited(|_, a| a), ) .subcommand( "private", ParentHandler::::new() .subcommand( "add", from_fn_async(add_private_domain::) .with_metadata("sync_db", Value::Bool(true)) .with_inherited(|_, a| a) .no_display() .with_about("about.add-private-domain-to-host") .with_call_remote::(), ) .subcommand( "remove", from_fn_async(remove_private_domain::) .with_metadata("sync_db", Value::Bool(true)) .with_inherited(|_, a| a) .no_display() .with_about("about.remove-private-domain-from-host") .with_call_remote::(), ) .with_inherited(|_, a| a), ) .with_inherited(Kind::inheritance), ) .subcommand( "list", from_fn_async(list_addresses::) .with_inherited(Kind::inheritance) .with_display_serializable() .with_custom_display_fn(|HandlerArgs { params, .. }, res| { use prettytable::*; if let Some(format) = params.format { display_serializable(format, res)?; return Ok(()); } let mut table = Table::new(); table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]); for entry in &res { if let Some(PublicDomainConfig { gateway, acme }) = &entry.public { table.add_row(row![ entry.address, &format!( "{} ({gateway})", if entry.private { "YES" } else { "ONLY" } ), acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE") ]); } else { table.add_row(row![entry.address, &format!("NO"), "N/A"]); } } table.print_tty(false)?; Ok(()) }) .with_about("about.list-addresses-for-host") .with_call_remote::(), ) } #[derive(Deserialize, Serialize, Parser)] pub struct AddPublicDomainParams { #[arg(help = "help.arg.fqdn")] pub fqdn: InternedString, #[arg(long, help = "help.arg.acme-provider")] pub acme: Option, #[arg(help = "help.arg.gateway-id")] pub gateway: GatewayId, } pub async fn add_public_domain( ctx: RpcContext, AddPublicDomainParams { fqdn, acme, gateway, }: AddPublicDomainParams, inheritance: Kind::Inheritance, ) -> Result, Error> { ctx.db .mutate(|db| { if let Some(acme) = &acme { if !db .as_public() .as_server_info() .as_network() .as_acme() .contains_key(&acme)? { return Err(Error::new(eyre!("unknown acme provider {}, please run acme.init for this provider first", acme.0), ErrorKind::InvalidRequest)); } } Kind::host_for(&inheritance, db)? .as_public_domains_mut() .insert(&fqdn, &PublicDomainConfig { acme, gateway })?; handle_duplicates(db) }) .await .result?; Kind::sync_host(&ctx, inheritance).await?; tokio::task::spawn_blocking(|| { crate::net::dns::query_dns(ctx, crate::net::dns::QueryDnsParams { fqdn }) }) .await .with_kind(ErrorKind::Unknown)? } #[derive(Deserialize, Serialize, Parser)] pub struct RemoveDomainParams { #[arg(help = "help.arg.fqdn")] pub fqdn: InternedString, } pub async fn remove_public_domain( ctx: RpcContext, RemoveDomainParams { fqdn }: RemoveDomainParams, inheritance: Kind::Inheritance, ) -> Result<(), Error> { ctx.db .mutate(|db| { Kind::host_for(&inheritance, db)? .as_public_domains_mut() .remove(&fqdn) }) .await .result?; Kind::sync_host(&ctx, inheritance).await?; Ok(()) } #[derive(Deserialize, Serialize, Parser)] pub struct AddPrivateDomainParams { #[arg(help = "help.arg.fqdn")] pub fqdn: InternedString, } pub async fn add_private_domain( ctx: RpcContext, AddPrivateDomainParams { fqdn }: AddPrivateDomainParams, inheritance: Kind::Inheritance, ) -> Result<(), Error> { ctx.db .mutate(|db| { Kind::host_for(&inheritance, db)? .as_private_domains_mut() .mutate(|d| Ok(d.insert(fqdn)))?; handle_duplicates(db) }) .await .result?; Kind::sync_host(&ctx, inheritance).await?; Ok(()) } pub async fn remove_private_domain( ctx: RpcContext, RemoveDomainParams { fqdn: domain }: RemoveDomainParams, inheritance: Kind::Inheritance, ) -> Result<(), Error> { ctx.db .mutate(|db| { Kind::host_for(&inheritance, db)? .as_private_domains_mut() .mutate(|d| Ok(d.remove(&domain))) }) .await .result?; Kind::sync_host(&ctx, inheritance).await?; Ok(()) } pub async fn list_addresses( ctx: RpcContext, _: Empty, inheritance: Kind::Inheritance, ) -> Result, Error> { Ok(Kind::host_for(&inheritance, &mut ctx.db.peek().await)? .de()? .addresses() .collect()) }