use crate::api_tokens::TokenID; use crate::constants; use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_rest_structures::net::NetworkName; use crate::libvirt_rest_structures::nw_filter::NetworkFilterName; use clap::Parser; use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::str::FromStr; /// VirtWeb backend API #[derive(Parser, Debug, Clone)] #[clap(author, version, about, long_about = None)] pub struct AppConfig { /// Read arguments from env file #[clap(short, long, env)] pub config: Option, /// Listen address #[clap(short, long, env, default_value = "0.0.0.0:8000")] pub listen_address: String, /// Website main origin #[clap(short, long, env, default_value = "http://localhost:3000")] pub website_origin: String, /// Additional allowed website origin /// /// Warning! These origins won't be usable for OpenID authentication, /// only for local auth #[clap(short = 'o', long, env)] pub additional_origins: Vec, /// Proxy IP, might end with a star "*" #[clap(short, long, env)] pub proxy_ip: Option, /// Secret key, used to sign some resources. Must be randomly generated #[clap(short = 'S', long, env, default_value = "")] secret: String, /// Specify whether the cookie should be transmitted only over secure connections #[clap(long, env)] pub cookie_secure: bool, /// Auth username #[arg(short = 'u', long, env, default_value = "admin")] pub auth_username: String, /// Auth password #[arg(short = 'P', long, env, default_value = "admin")] pub auth_password: String, /// Disable authentication WARNING! THIS IS UNSECURE, it was designed only for development /// purpose, it should NEVER be used in production #[arg(long, env)] pub unsecure_disable_auth: bool, /// Disable local auth #[arg(long, env)] pub disable_local_auth: bool, /// Request header that can be added by a reverse proxy to disable local authentication #[arg(long, env, default_value = "X-Disable-Local-Auth")] pub disable_auth_header_token: String, /// URL where the OpenID configuration can be found #[arg( long, env, default_value = "http://localhost:9001/dex/.well-known/openid-configuration" )] pub oidc_configuration_url: String, /// Disable OpenID authentication #[arg(long, env)] pub disable_oidc: bool, /// OpenID client ID #[arg(long, env, default_value = "foo")] pub oidc_client_id: String, /// OpenID client secret #[arg(long, env, default_value = "bar")] pub oidc_client_secret: String, /// OpenID login redirect URL #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")] oidc_redirect_url: String, /// Storage directory #[arg(short, long, env, default_value = "storage")] pub storage: String, /// Directory where temporary files are stored /// /// Warning! This directory MUST be changed if `/tmp` is not in the same disk as the storage /// directory! #[arg(short, long, env, default_value = "/tmp")] pub temp_dir: String, /// Hypervisor URI. If not specified, "" will be used instead #[arg(short = 'H', long, env)] pub hypervisor_uri: Option, /// Trusted network. If set, a client (user) from a different network will not be able to perform /// request other than those with GET verb (aside for login) #[arg(short = 'T', long, env)] pub trusted_network: Vec, /// Comma-separated list of allowed networks. If set, a client (user or API token) from a /// different network will not be able to access VirtWeb #[arg(short = 'A', long, env)] pub allowed_networks: Vec, } lazy_static::lazy_static! { static ref ARGS: AppConfig = { AppConfig::parse() }; } impl AppConfig { /// Parse environment variables from file, if requedst pub fn parse_env_file() -> anyhow::Result<()> { if let Some(c) = Self::parse().config { log::info!("Load additional environment variables from {c}"); let conf_file = Path::new(&c); if !conf_file.is_file() { panic!("Specified configuration is not a file!"); } dotenvy::from_path(conf_file)?; } Ok(()) } /// Get parsed command line arguments pub fn get() -> &'static AppConfig { &ARGS } /// Get auth cookie domain pub fn cookie_domain(&self) -> Option { if cfg!(debug_assertions) { let domain = self.website_origin.split_once("://")?.1; Some( domain .split_once(':') .map(|s| s.0) .unwrap_or(domain) .to_string(), ) } else { // In release mode, the web app is hosted on the same origin as the API None } } /// Get app secret pub fn secret(&self) -> &str { let mut secret = self.secret.as_str(); if cfg!(debug_assertions) && secret.is_empty() { secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY"; } if secret.is_empty() { panic!("SECRET is undefined or too short (min 64 chars)!") } secret } /// Check out whether provided credentials are valid or not for local authentication pub fn check_local_login(&self, user: &str, pass: &str) -> bool { self.auth_username == user && self.auth_password == pass } /// Check if an IP belongs to a trusted network or not pub fn is_trusted_ip(&self, ip: IpAddr) -> bool { if self.trusted_network.is_empty() { return true; } for i in &self.trusted_network { let net = ipnetwork::IpNetwork::from_str(i).expect("Trusted network is invalid!"); if net.contains(ip) { return true; } } false } /// Check if an IP belongs to an allowed network or not pub fn is_allowed_ip(&self, ip: IpAddr) -> bool { if self.allowed_networks.is_empty() { return true; } for i in &self.allowed_networks { for sub_i in i.split(',') { let net = ipnetwork::IpNetwork::from_str(sub_i).expect("Allowed network is invalid!"); if net.contains(ip) { return true; } } } false } /// Get OpenID providers configuration pub fn openid_provider(&self) -> Option> { if self.disable_oidc { return None; } Some(OIDCProvider { client_id: self.oidc_client_id.as_str(), client_secret: self.oidc_client_secret.as_str(), configuration_url: self.oidc_configuration_url.as_str(), }) } /// Get OIDC callback URL pub fn oidc_redirect_url(&self) -> String { self.oidc_redirect_url .replace("APP_ORIGIN", &self.website_origin) } /// Get root storage directory pub fn storage_path(&self) -> PathBuf { let storage_path = Path::new(&self.storage); if !storage_path.is_dir() { panic!( "Specified storage path ({}) is not a directory!", self.storage ); } storage_path.canonicalize().unwrap() } /// Get iso storage directory pub fn iso_storage_path(&self) -> PathBuf { self.storage_path().join("iso") } /// Get VM vnc sockets directory pub fn vnc_sockets_path(&self) -> PathBuf { self.storage_path().join("vnc") } /// Get VM vnc sockets path for domain pub fn vnc_socket_for_domain(&self, name: &str) -> PathBuf { self.vnc_sockets_path().join(format!("vnc-{}", name)) } /// Get VM vnc sockets directory pub fn disks_storage_path(&self) -> PathBuf { self.storage_path().join("disks") } pub fn vm_storage_path(&self, id: XMLUuid) -> PathBuf { self.disks_storage_path().join(id.as_string()) } pub fn definitions_path(&self) -> PathBuf { self.storage_path().join("definitions") } pub fn vm_definition_path(&self, name: &str) -> PathBuf { self.definitions_path().join(format!("vm-{name}.json")) } pub fn net_definition_path(&self, name: &NetworkName) -> PathBuf { self.definitions_path().join(format!("net-{}.json", name.0)) } pub fn nat_path(&self) -> PathBuf { self.storage_path().join(constants::STORAGE_NAT_DIR) } pub fn net_nat_path(&self, name: &NetworkName) -> PathBuf { self.nat_path().join(name.nat_file_name()) } pub fn net_filter_definition_path(&self, name: &NetworkFilterName) -> PathBuf { self.definitions_path() .join(format!("nwfilter-{}.json", name.0)) } pub fn api_tokens_path(&self) -> PathBuf { self.storage_path().join(constants::STORAGE_TOKENS_DIR) } pub fn api_token_definition_path(&self, id: TokenID) -> PathBuf { self.api_tokens_path().join(format!("{}.json", id.0)) } } #[derive(Debug, Clone, serde::Serialize)] pub struct OIDCProvider<'a> { #[serde(skip_serializing)] pub client_id: &'a str, #[serde(skip_serializing)] pub client_secret: &'a str, #[serde(skip_serializing)] pub configuration_url: &'a str, } #[cfg(test)] mod test { use crate::app_config::AppConfig; #[test] fn verify_cli() { use clap::CommandFactory; AppConfig::command().debug_assert() } }