#[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConfigSection { Listeners, DockerAgents, Http2, Pages, Cache, Transform, Forwarding, Headers, Prometheus, Geoip, Administration, Tls, Vhosts, WebServices, PhpFpmProfiles, Redirects, NotFoundHosts, Streams, AclProfiles, HostPolicies, Response, Logging, GlobalSecurity, } impl ConfigSection { fn from_name(name: &str) -> Option { match name { "listeners" => Some(Self::Listeners), "docker-agents" => Some(Self::DockerAgents), "http2" => Some(Self::Http2), "pages" => Some(Self::Pages), "cache" => Some(Self::Cache), "transform" => Some(Self::Transform), "forwarding" => Some(Self::Forwarding), "headers" => Some(Self::Headers), "prometheus" => Some(Self::Prometheus), "geoip" => Some(Self::Geoip), "administration" => Some(Self::Administration), "tls" => Some(Self::Tls), "vhosts" => Some(Self::Vhosts), "web-services" => Some(Self::WebServices), "php-fpm-profiles" => Some(Self::PhpFpmProfiles), "redirects" => Some(Self::Redirects), "not-found-hosts" => Some(Self::NotFoundHosts), "streams" => Some(Self::Streams), "acl-profiles" => Some(Self::AclProfiles), "host-policies" => Some(Self::HostPolicies), "response" => Some(Self::Response), "logging" => Some(Self::Logging), "global-security" => Some(Self::GlobalSecurity), _ => None, } } fn read(self, config: &crate::config::Config) -> Value { match self { Self::Listeners => json!(&config.listeners), Self::DockerAgents => json!(&config.docker_agents), Self::Http2 => json!(&config.http2), Self::Pages => json!(&config.pages), Self::Cache => json!(&config.cache), Self::Transform => json!(&config.transform), Self::Forwarding => json!(&config.forwarding), Self::Headers => json!(&config.headers), Self::Prometheus => json!(&config.prometheus), Self::Geoip => json!(&config.geoip_updates), Self::Administration => json!(&config.administration), Self::Tls => json!(&config.tls), Self::Vhosts => json!(&config.vhosts), Self::WebServices => json!(&config.web_services), Self::PhpFpmProfiles => json!(&config.php_fpm_profiles), Self::Redirects => json!(&config.redirects), Self::NotFoundHosts => json!(&config.not_found_hosts), Self::Streams => json!(&config.streams), Self::AclProfiles => json!(&config.acl_profiles), Self::HostPolicies => json!(&config.host_policies), Self::Response => json!({ "server_header": &config.server_header, "diagnostic_headers": &config.diagnostic_headers, "compression": &config.compression, }), Self::Logging => json!({ "logging": &config.logging, "error_log": &config.error_log, }), Self::GlobalSecurity => json!(&config.global_security), } } fn replace( self, config: &mut crate::config::Config, name: &str, value: Value, ) -> Result<(), ApiError> { match self { Self::Listeners => { config.listeners = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::DockerAgents => { config.docker_agents = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Http2 => { config.http2 = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Pages => { config.pages = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Cache => { config.cache = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Transform => { config.transform = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Forwarding => { config.forwarding = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Headers => { config.headers = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Prometheus => { config.prometheus = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Geoip => { config.geoip_updates = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Administration => { config.administration = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Tls => { config.tls = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Vhosts => { config.vhosts = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::WebServices => { config.web_services = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::PhpFpmProfiles => { config.php_fpm_profiles = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Redirects => { config.redirects = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::NotFoundHosts => { config.not_found_hosts = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Streams => { config.streams = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::AclProfiles => { config.acl_profiles = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::HostPolicies => { config.host_policies = serde_json::from_value(value).map_err(|error| invalid_section(name, error))? } Self::Response => { let part: ResponseSection = serde_json::from_value(value).map_err(|error| invalid_section(name, error))?; config.server_header = part.server_header; config.diagnostic_headers = part.diagnostic_headers; config.compression = part.compression; } Self::Logging => { let part: LoggingSection = serde_json::from_value(value).map_err(|error| invalid_section(name, error))?; config.logging = part.logging; config.error_log = part.error_log; } Self::GlobalSecurity => { let part: crate::config::GlobalSecurity = serde_json::from_value(value).map_err(|error| invalid_section(name, error))?; if part.user_agent_blocking.is_none() { return Err(ApiError( StatusCode::BAD_REQUEST, "global-security requires user_agent_blocking in its complete section value".into(), )); } config.global_security = part; } } Ok(()) } } fn requested_config_section(name: &str) -> Result { ConfigSection::from_name(name).ok_or_else(|| { ApiError( StatusCode::NOT_FOUND, "unknown configuration section".into(), ) }) } async fn configuration_section( State(state): State>, Path(section): Path, ) -> Result, ApiError> { let selected = requested_config_section(§ion)?; let snapshot = state.config.load(); Ok(Json(json!({ "revision": snapshot.envelope.revision, "section": section, "value": selected.read(&snapshot.envelope.config), }))) } async fn update_section( State(state): State>, Extension(session): Extension, Path(section): Path, Json(input): Json, ) -> Result, ApiError> { let selected = requested_config_section(§ion)?; let current = state.config.load_full(); if current.envelope.revision != input.expected_revision { return Err(ApiError( StatusCode::CONFLICT, "configuration changed; reload before saving".into(), )); } let mut config = current.envelope.config.clone(); selected.replace(&mut config, §ion, input.value)?; state .update_as( Update { expected_revision: input.expected_revision, config, }, session.username, ) .await .map(Json) } async fn geoip_update( State(state): State>, Extension(session): Extension, ) -> Result, ApiError> { let result = crate::geoip::update_now(&state).await.map_err(|error| { tracing::warn!(error = %format!("{error:#}"), "manual GeoIP update failed"); ApiError( StatusCode::BAD_GATEWAY, format!("GeoIP update failed: {error:#}"), ) })?; state .store .audit( &session.username, "geoip.update", &format!( "outcome={}; files={}", result.outcome, if result.updated_files.is_empty() { "none".to_string() } else { result.updated_files.join(",") } ), ) .await .map_err(auth::internal)?; Ok(Json(result)) }