//! Hyper based HTTP server for Krill. //! use std::{ collections::HashMap, convert::{Infallible, TryInto}, env, fs::File, io::Read, net::SocketAddr, path::{Path, PathBuf}, process, str::FromStr, sync::Arc, }; use bytes::Bytes; use serde::Serialize; use hyper::{ header::HeaderName, http::HeaderValue, server::conn::AddrIncoming, service::{make_service_fn, service_fn}, Method, }; use tokio::select; use tokio::signal::unix::SignalKind; use rpki::{ ca::{ idexchange, idexchange::{CaHandle, ChildHandle, ParentHandle, PublisherHandle}, }, repository::resources::Asn, }; use crate::{ commons::{ api::{ ApiRepositoryContact, AspaDefinitionUpdates, BgpStats, CommandHistoryCriteria, ParentCaReq, PublisherList, RepositoryContact, RoaConfigurationUpdates, RtaName, Token, }, bgp::BgpAnalysisAdvice, error::Error, eventsourcing::AggregateStoreError, util::file, }, constants::{ KRILL_ENV_HTTP_LOG_INFO, KRILL_ENV_UPGRADE_ONLY, KRILL_VERSION_MAJOR, KRILL_VERSION_MINOR, KRILL_VERSION_PATCH, NO_RESOURCE, }, daemon::{ auth::common::permissions::Permission, auth::{Auth, Handle}, ca::CaStatus, config::Config, http::{ auth::auth, statics::statics, testbed::testbed, tls, tls_keys, HttpResponse, Request, RequestPath, RoutingResult, }, krillserver::KrillServer, ta::{self, TA_NAME}, }, upgrades::{finalise_data_migration, post_start_upgrade, prepare_upgrade_data_migrations, UpgradeMode}, }; //------------ State ----------------------------------------------------- pub type State = Arc; fn print_write_error_hint_and_die(error_msg: String) { eprintln!("{}", error_msg); eprintln!(); eprintln!("Hint: if you use systemd you may need to override the allowed ReadWritePaths,"); eprintln!("the easiest way may be by doing 'systemctl edit krill' and add a section like:"); eprintln!(); eprintln!("[Service]"); eprintln!("ReadWritePaths=/local/path1 /local/path2 ..."); } fn write_pid_file_or_die(config: &Config) { let pid_file = config.pid_file(); if let Err(e) = file::save(process::id().to_string().as_bytes(), &pid_file) { print_write_error_hint_and_die(format!("Could not write PID file: {}", e)); } } fn test_data_dir_or_die(config_item: &str, dir: &Path) { let test_file = dir.join("test"); if let Err(e) = file::save(b"test", &test_file) { print_write_error_hint_and_die(format!( "Cannot write to dir '{}' for configuration setting '{}', Error: {}", dir.to_string_lossy(), config_item, e )); } else if let Err(e) = file::delete_file(&test_file) { print_write_error_hint_and_die(format!( "Cannot delete test file '{}' in dir for configuration setting '{}', Error: {}", test_file.to_string_lossy(), config_item, e )); } } fn test_data_dirs_or_die(config: &Config) { test_data_dir_or_die("data_dir", &config.data_dir); if let Some(rfc8181_log_dir) = &config.rfc8181_log_dir { test_data_dir_or_die("rfc8181_log_dir", rfc8181_log_dir); } if let Some(rfc6492_log_dir) = &config.rfc6492_log_dir { test_data_dir_or_die("rfc6492_log_dir", rfc6492_log_dir); } } pub async fn start_krill_daemon(config: Arc) -> Result<(), Error> { let optional_lock = if config.data_dir_use_lock { Some(KrillLock::create(&config)) } else { None }; write_pid_file_or_die(&config); test_data_dirs_or_die(&config); // Call upgrade, this will only do actual work if needed. let upgrade_report = prepare_upgrade_data_migrations(UpgradeMode::PrepareToFinalise, config.clone())?; if let Some(report) = &upgrade_report { if report.data_migration() { finalise_data_migration(report.versions(), config.as_ref()).map_err(|e| { Error::Custom(format!( "Finishing prepared migration failed unexpectedly. Please check your data directory {}. If you find folders named 'arch-cas-{}' or 'arch-pubd-{}' there, then rename them to 'cas' and 'pubd' respectively and re-install krill version {}. Underlying error was: {}", config.data_dir.to_string_lossy(), report.versions().from(), report.versions().from(), report.versions().from(), e )) })?; } } // Create the server, this will create the necessary data sub-directories if needed let krill_server = KrillServer::build(config.clone()).await?; // Call post-start upgrades to trigger any upgrade related runtime actions, such as // re-issuing ROAs because subject name strategy has changed. if let Some(report) = upgrade_report { post_start_upgrade(report.versions(), &krill_server).await?; } // If the operator wanted to do the upgrade only, now is a good time to report success and stop if env::var(KRILL_ENV_UPGRADE_ONLY).is_ok() { println!("Krill upgrade successful"); std::process::exit(0); } // Build the scheduler which will be responsible for executing planned/triggered tasks let scheduler = krill_server.build_scheduler(); let scheduler_future = scheduler.run(); // Start creating the server. let krill_server = Arc::new(krill_server); // Create self-signed HTTPS cert if configured and not generated earlier. if config.https_mode().is_generate_https_cert() { tls_keys::create_key_cert_if_needed(&config.data_dir).map_err(|e| Error::HttpsSetup(format!("{}", e)))?; } // Start a hyper server for the configured socket. let server_futures = futures_util::future::select_all( config .socket_addresses() .into_iter() .map(|socket_addr| tokio::spawn(single_http_listener(krill_server.clone(), socket_addr, config.clone()))), ); if let Some(lock) = optional_lock { #[cfg(not(unix))] select!( _ = server_futures => error!("http server stopped unexpectedly"), _ = scheduler_future => error!("scheduler stopped unexpectedly"), _ = lock.handle_ctrl_c() => info!("ctrl-c received"), ); #[cfg(unix)] select!( _ = server_futures => error!("http server stopped unexpectedly"), _ = scheduler_future => error!("scheduler stopped unexpectedly"), _ = lock.handle_ctrl_c() => info!("ctrl-c received"), _ = lock.handle_sig_term() => info!("sig TERM received"), ); } else { select!( _ = server_futures => error!("http server stopped unexpectedly"), _ = scheduler_future => error!("scheduler stopped unexpectedly"), ); } Err(Error::custom("stopping krill process")) } async fn single_http_listener(krill_server: Arc, socket_addr: SocketAddr, config: Arc) { // See if we can bind to the configured address and port first. let incoming = match AddrIncoming::bind(&socket_addr) { Err(e) => { error!("Could not bind to address and port: {}, Error: {}", &socket_addr, e); return; } Ok(incoming) => incoming, }; if config.https_mode().is_disable_https() { // Make a service function. let service = make_service_fn(|_| { let krill_server = krill_server.clone(); async move { Ok::<_, Infallible>(service_fn(move |req: hyper::Request| { let krill_server = krill_server.clone(); map_requests(req, krill_server) })) } }); if let Err(e) = hyper::Server::builder(incoming).serve(service).await { error!("Fatal server error: {}", e) } } else { // Set up a TLS acceptor to use. let server_config_builder = tls::TlsConfigBuilder::new() .cert_path(tls_keys::cert_file_path(&config.data_dir)) .key_path(tls_keys::key_file_path(&config.data_dir)); let server_config = server_config_builder.build().unwrap(); let acceptor = tls::TlsAcceptor::new(server_config, incoming); // Make a service function. We have to do this again because of hyper types.. // It won't like a service made for a Server that is not of the type of the // TlsAcceptor we are about to set up. let service = make_service_fn(|_| { let krill_server = krill_server.clone(); async move { Ok::<_, Infallible>(service_fn(move |req: hyper::Request| { let krill_server = krill_server.clone(); map_requests(req, krill_server) })) } }); if let Err(e) = hyper::Server::builder(acceptor).serve(service).await { error!("Fatal server error: {}", e) } } } struct RequestLogger { req_method: hyper::Method, req_path: String, } impl RequestLogger { fn begin(req: &hyper::Request) -> Self { let req_method = req.method().clone(); let req_path = RequestPath::from_request(req).full().to_string(); if log_enabled!(log::Level::Trace) { trace!( "Request: method={} path={} headers={:?}", &req_method, &req_path, &req.headers() ); } RequestLogger { req_method, req_path } } fn end(&self, res: Result<&HttpResponse, &Error>) { match res { Ok(response) => { match (response.status(), response.benign(), response.cause()) { (s, false, Some(cause)) if s.is_client_error() => warn!("HTTP {}: {}", s.as_u16(), cause), (s, false, Some(cause)) if s.is_server_error() => error!("HTTP {}: {}", s.as_u16(), cause), _ => {} } if env::var(KRILL_ENV_HTTP_LOG_INFO).is_ok() { info!("{} {} {}", self.req_method, self.req_path, response.status()); } else { debug!("{} {} {}", self.req_method, self.req_path, response.status()); } if response.loggable() && log_enabled!(log::Level::Trace) { trace!("Response: headers={:?} body={:?}", response.headers(), response.body()); } } Err(err) => { error!("{} {} Error: {}", self.req_method, self.req_path, err); } } } } async fn map_requests(req: hyper::Request, state: State) -> Result, Error> { let logger = RequestLogger::begin(&req); let req = Request::new(req, state).await; // Save any updated auth details, e.g. if an OpenID Connect token needed // refreshing. let new_auth = req.actor().new_auth(); // We used to use .or_else() here but that causes a large recursive call // tree due to these calls being to async functions, large enough with the // given Request object passed each time that it eventually resulted in // stack overflow. By doing it by hand like this we avoid the use of the // macros that cause the recursion. We could also look at putting less data // on the stack. let mut res = api(req).await; if let Err(req) = res { res = auth(req).await; } if let Err(req) = res { res = health(req).await; } if let Err(req) = res { res = metrics(req).await; } if let Err(req) = res { res = stats(req).await; } if let Err(req) = res { res = rfc8181(req).await; } if let Err(req) = res { res = rfc6492(req).await; } if let Err(req) = res { res = ta(req).await; } if let Err(req) = res { res = rrdp(req).await; } if let Err(req) = res { res = testbed(req).await; } if let Err(req) = res { res = statics(req).await; } if res.is_err() { // catch all to the UI res = Ok(HttpResponse::html(super::statics::INDEX)); } // Not found responses are actually a special Ok result.. let res = res.map_err(|_| Error::custom("should have received not found response")); // Augment the response with any updated auth details that were determined above. let res = add_new_auth_to_response(res, new_auth); // Log the request and the response. logger.end(res.as_ref()); res.map(|res| res.response()) } //------------ Support Functions --------------------------------------------- /// HTTP redirects cannot have a response body and so we cannot render the error /// to be displayed in Lagosta as a JSON body, instead we must package the JSON /// as a query parameter. pub fn render_error_redirect(err: Error) -> RoutingResult { let response = err.to_error_response(); let json = serde_json::to_string(&response).or_else(|err| { Ok(format!( "JSON serialization error while processing internal error: {}", err )) })?; let b64 = base64::encode(json); let location = format!("/ui/login?error={}", b64); Ok(HttpResponse::found(&location)) } pub fn render_empty_res(res: Result<(), Error>) -> RoutingResult { match res { Ok(()) => render_ok(), Err(e) => render_error(e), } } #[allow(clippy::unnecessary_wraps)] fn render_error(e: Error) -> RoutingResult { debug!("Server Error: {}", e); Ok(HttpResponse::response_from_error(e)) } #[allow(clippy::unnecessary_wraps)] fn render_json(obj: O) -> RoutingResult { Ok(HttpResponse::json(&obj)) } fn render_json_res(res: Result) -> RoutingResult { match res { Ok(o) => render_json(o), Err(e) => render_error(e), } } /// A clean 404 result for the API (no content, not for humans) #[allow(clippy::unnecessary_wraps)] fn render_unknown_resource() -> RoutingResult { Ok(HttpResponse::response_from_error(Error::ApiUnknownResource)) } /// A clean 200 result for the API (no content, not for humans) #[allow(clippy::unnecessary_wraps)] pub fn render_ok() -> RoutingResult { Ok(HttpResponse::ok()) } #[allow(clippy::unnecessary_wraps)] pub fn render_unknown_method() -> RoutingResult { Ok(HttpResponse::response_from_error(Error::ApiUnknownMethod)) } /// A clean 404 response #[allow(clippy::unnecessary_wraps)] pub async fn render_not_found(_req: Request) -> RoutingResult { Ok(HttpResponse::not_found()) } /// Returns the server health. pub async fn health(req: Request) -> RoutingResult { if req.is_get() && req.path().segment() == "health" { render_ok() } else { Err(req) } } /// Produce prometheus style metrics #[allow(clippy::format_push_string)] pub async fn metrics(req: Request) -> RoutingResult { if req.is_get() && req.path().segment().starts_with("metrics") { let server = req.state(); struct AllBgpStats { announcements_valid: HashMap, announcements_invalid_asn: HashMap, announcements_invalid_length: HashMap, announcements_not_found: HashMap, roas_too_permissive: HashMap, roas_redundant: HashMap, roas_stale: HashMap, roas_total: HashMap, } impl AllBgpStats { fn add_ca(&mut self, ca: &CaHandle, stats: &BgpStats) { self.announcements_valid.insert(ca.clone(), stats.announcements_valid); self.announcements_invalid_asn .insert(ca.clone(), stats.announcements_invalid_asn); self.announcements_invalid_length .insert(ca.clone(), stats.announcements_invalid_length); self.announcements_not_found .insert(ca.clone(), stats.announcements_not_found); self.roas_too_permissive.insert(ca.clone(), stats.roas_too_permissive); self.roas_redundant.insert(ca.clone(), stats.roas_redundant); self.roas_stale.insert(ca.clone(), stats.roas_stale); self.roas_total.insert(ca.clone(), stats.roas_total); } } let mut res = String::new(); let info = server.server_info(); res.push_str("# HELP krill_server_start unix timestamp in seconds of last krill server start\n"); res.push_str("# TYPE krill_server_start gauge\n"); res.push_str(&format!("krill_server_start {}\n", info.started())); res.push('\n'); res.push_str("# HELP krill_version_major krill server major version number\n"); res.push_str("# TYPE krill_version_major gauge\n"); res.push_str(&format!("krill_version_major {}\n", KRILL_VERSION_MAJOR)); res.push('\n'); res.push_str("# HELP krill_version_minor krill server minor version number\n"); res.push_str("# TYPE krill_version_minor gauge\n"); res.push_str(&format!("krill_version_minor {}\n", KRILL_VERSION_MINOR)); res.push('\n'); res.push_str("# HELP krill_version_patch krill server patch version number\n"); res.push_str("# TYPE krill_version_patch gauge\n"); res.push_str(&format!("krill_version_patch {}\n", KRILL_VERSION_PATCH)); #[cfg(feature = "multi-user")] { res.push('\n'); res.push_str("# HELP krill_auth_session_cache_size total number of cached login session tokens\n"); res.push_str("# TYPE krill_auth_session_cache_size gauge\n"); res.push_str(&format!( "krill_auth_session_cache_size {}\n", server.login_session_cache_size() )); } if let Ok(cas_stats) = server.cas_stats().await { let number_cas = cas_stats.len(); res.push('\n'); res.push_str("# HELP krill_cas number of cas in krill\n"); res.push_str("# TYPE krill_cas gauge\n"); res.push_str(&format!("krill_cas {}\n", number_cas)); if !server.config.metrics.metrics_hide_ca_details { // Show per CA details let mut ca_status_map: HashMap = HashMap::new(); for ca in cas_stats.keys() { if let Ok(ca_status) = server.ca_status(ca).await { ca_status_map.insert(ca.clone(), ca_status); } } { // CA -> Parent metrics // krill_ca_parent_success{{ca="ca", parent="parent"}} 1 // krill_ca_parent_last_success_time{{ca="ca", parent="parent"}} 1630921599 // timestamp res.push('\n'); res.push_str( "# HELP krill_ca_parent_success status of last CA to parent connection (0=issue, 1=success)\n", ); res.push_str("# TYPE krill_ca_parent_success gauge\n"); for (ca, status) in ca_status_map.iter() { if ca.as_str() != TA_NAME { for (parent, status) in status.parents().iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. if let Some(exchange) = status.last_exchange() { res.push_str(&format!( "krill_ca_parent_success{{ca=\"{}\", parent=\"{}\"}} {}\n", ca, parent, i32::from(exchange.was_success()) )); } } } } res.push('\n'); res.push_str( "# HELP krill_ca_parent_last_success_time unix timestamp in seconds of last successful CA to parent connection\n", ); res.push_str("# TYPE krill_ca_parent_last_success_time gauge\n"); for (ca, status) in ca_status_map.iter() { if ca.as_str() != TA_NAME { for (parent, status) in status.parents().iter() { // skip the ones for which we have no successful connection at all. Most likely // they were just added (in which case it will come) - or were never successful // in which case the metric above will say that the status is 0 if let Some(last_success) = status.last_success() { res.push_str(&format!( "krill_ca_parent_last_success_time{{ca=\"{}\", parent=\"{}\"}} {}\n", ca, parent, last_success )); } } } } } { // CA -> Publication Server status // krill_ca_repo_success{{ca="ca"}} 1 // krill_ca_repo_last_success_time{{ca="ca"}} 1630921599 // krill_ca_repo_next_before_time{{ca="ca"}} 1630921599 res.push('\n'); res.push_str("# HELP krill_ca_ps_success status of last CA to Publication Server connection (0=issue, 1=success)\n"); res.push_str("# TYPE krill_ca_ps_success gauge\n"); for (ca, status) in ca_status_map.iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. if let Some(exchange) = status.repo().last_exchange() { res.push_str(&format!( "krill_ca_ps_success{{ca=\"{}\"}} {}\n", ca, i32::from(exchange.was_success()) )); } } res.push('\n'); res.push_str("# HELP krill_ca_ps_last_success_time unix timestamp in seconds of last successful CA to Publication Server connection\n"); res.push_str("# TYPE krill_ca_ps_last_success_time gauge\n"); for (ca, status) in ca_status_map.iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. if let Some(last_success) = status.repo().last_success() { res.push_str(&format!( "krill_ca_ps_last_success_time{{ca=\"{}\"}} {}\n", ca, last_success )); } } } // Do not show child metrics if none of the CAs has any children.. // Many users do not delegate so, showing these metrics would just be confusing. let any_children = cas_stats.values().any(|ca| ca.child_count() > 0); if any_children && !server.config.metrics.metrics_hide_child_details { // CA -> Children // krill_cas_children{ca="parent"} 11 // nr of children // krill_ca_child_success{ca="parent", child="child"} 1 // krill_ca_child_state{ca="parent", child="child"} 1 // krill_ca_child_last_connection{ca="parent", child="child"} 1630921599 // krill_ca_child_last_success{ca="parent", child="child"} 1630921599 // krill_ca_child_agent_total{ca="parent", ua="krill/0.9.2"} 11 res.push('\n'); res.push_str("# HELP krill_cas_children number of children for CA\n"); res.push_str("# TYPE krill_cas_children gauge\n"); for (ca, status) in cas_stats.iter() { res.push_str(&format!( "krill_cas_children{{ca=\"{}\"}} {}\n", ca, status.child_count() )); } res.push('\n'); res.push_str( "# HELP krill_ca_child_success status of last child to CA connection (0=issue, 1=success)\n", ); res.push_str("# TYPE krill_ca_child_success gauge\n"); for (ca, status) in ca_status_map.iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. for (child, status) in status.children().iter() { if let Some(exchange) = status.last_exchange() { res.push_str(&format!( "krill_ca_child_success{{ca=\"{}\", child=\"{}\"}} {}\n", ca, child, i32::from(exchange.was_success()) )); } } } res.push('\n'); res.push_str( "# HELP krill_ca_child_state child state (see 'suspend_child_after_inactive_hours' config) (0=suspended, 1=active)\n", ); res.push_str("# TYPE krill_ca_child_state gauge\n"); for (ca, status) in ca_status_map.iter() { for (child, status) in status.children().iter() { res.push_str(&format!( "krill_ca_child_state{{ca=\"{}\", child=\"{}\"}} {}\n", ca, child, i32::from(status.suspended().is_none()) )); } } res.push('\n'); res.push_str("# HELP krill_ca_child_last_connection unix timestamp in seconds of last child to CA connection\n"); res.push_str("# TYPE krill_ca_child_last_connection gauge\n"); for (ca, status) in ca_status_map.iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. for (child, status) in status.children().iter() { if let Some(exchange) = status.last_exchange() { let timestamp = exchange.timestamp(); res.push_str(&format!( "krill_ca_child_last_connection{{ca=\"{}\", child=\"{}\"}} {}\n", ca, child, timestamp )); } } } res.push('\n'); res.push_str( "# HELP krill_ca_child_last_success unix timestamp in seconds of last successful child to CA connection\n", ); res.push_str("# TYPE krill_ca_child_last_success gauge\n"); for (ca, status) in ca_status_map.iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. for (child, status) in status.children().iter() { if let Some(time) = status.last_success() { res.push_str(&format!( "krill_ca_child_last_success{{ca=\"{}\", child=\"{}\"}} {}\n", ca, child, time )); } } } res.push('\n'); res.push_str( "# HELP krill_ca_child_agent_total total children per user agent based on their last connection\n", ); res.push_str("# TYPE krill_ca_child_agent_total gauge\n"); for (ca, status) in ca_status_map.iter() { // skip the ones for which we have no status yet, i.e it was really only just added // and no attempt to connect has yet been made. let mut user_agent_totals: HashMap = HashMap::new(); for status in status.children().values() { if let Some(exchange) = status.last_exchange() { let agent = exchange.user_agent().cloned().unwrap_or_else(|| "".to_string()); *user_agent_totals.entry(agent).or_insert(0) += 1; } } for (ua, total) in user_agent_totals.iter() { res.push_str(&format!( "krill_ca_child_agent_total{{ca=\"{}\", user_agent=\"{}\"}} {}\n", ca, ua, total )); } } } if !server.config.metrics.metrics_hide_roa_details { // BGP Announcement metrics // Aggregate ROA vs BGP stats per status let mut all_bgp_stats = AllBgpStats { announcements_valid: HashMap::new(), announcements_invalid_asn: HashMap::new(), announcements_invalid_length: HashMap::new(), announcements_not_found: HashMap::new(), roas_too_permissive: HashMap::new(), roas_redundant: HashMap::new(), roas_stale: HashMap::new(), roas_total: HashMap::new(), }; for (ca, ca_stats) in cas_stats.iter() { all_bgp_stats.add_ca(ca, ca_stats.bgp_stats()); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_announcements_valid number of announcements seen for CA resources with RPKI state VALID\n"); res.push_str("# TYPE krill_cas_bgp_announcements_valid gauge\n"); for (ca, nr) in all_bgp_stats.announcements_valid.iter() { res.push_str(&format!("krill_cas_bgp_announcements_valid{{ca=\"{}\"}} {}\n", ca, nr)); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_announcements_invalid_asn number of announcements seen for CA resources with RPKI state INVALID (ASN mismatch)\n"); res.push_str("# TYPE krill_cas_bgp_announcements_invalid_asn gauge\n"); for (ca, nr) in all_bgp_stats.announcements_invalid_asn.iter() { res.push_str(&format!( "krill_cas_bgp_announcements_invalid_asn{{ca=\"{}\"}} {}\n", ca, nr )); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_announcements_invalid_length number of announcements seen for CA resources with RPKI state INVALID (prefix exceeds max length)\n"); res.push_str("# TYPE krill_cas_bgp_announcements_invalid_length gauge\n"); for (ca, nr) in all_bgp_stats.announcements_invalid_length.iter() { res.push_str(&format!( "krill_cas_bgp_announcements_invalid_length{{ca=\"{}\"}} {}\n", ca, nr )); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_announcements_not_found number of announcements seen for CA resources with RPKI state NOT FOUND (none of the CA's ROAs cover this)\n"); res.push_str("# TYPE krill_cas_bgp_announcements_not_found gauge\n"); for (ca, nr) in all_bgp_stats.announcements_not_found.iter() { res.push_str(&format!( "krill_cas_bgp_announcements_not_found{{ca=\"{}\"}} {}\n", ca, nr )); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_roas_too_permissive number of ROAs for this CA which allow excess announcements (0 may also indicate that no BGP info is available)\n"); res.push_str("# TYPE krill_cas_bgp_roas_too_permissive gauge\n"); for (ca, nr) in all_bgp_stats.roas_too_permissive.iter() { res.push_str(&format!("krill_cas_bgp_roas_too_permissive{{ca=\"{}\"}} {}\n", ca, nr)); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_roas_redundant number of ROAs for this CA which are redundant (0 may also indicate that no BGP info is available)\n"); res.push_str("# TYPE krill_cas_bgp_roas_redundant gauge\n"); for (ca, nr) in all_bgp_stats.roas_redundant.iter() { res.push_str(&format!("krill_cas_bgp_roas_redundant{{ca=\"{}\"}} {}\n", ca, nr)); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_roas_stale number of ROAs for this CA for which no announcements are seen (0 may also indicate that no BGP info is available)\n"); res.push_str("# TYPE krill_cas_bgp_roas_stale gauge\n"); for (ca, nr) in all_bgp_stats.roas_stale.iter() { res.push_str(&format!("krill_cas_bgp_roas_stale{{ca=\"{}\"}} {}\n", ca, nr)); } res.push('\n'); res.push_str("# HELP krill_cas_bgp_roas_total total number of ROAs for this CA\n"); res.push_str("# TYPE krill_cas_bgp_roas_stale gauge\n"); for (ca, nr) in all_bgp_stats.roas_total.iter() { res.push_str(&format!("krill_cas_bgp_roas_total{{ca=\"{}\"}} {}\n", ca, nr)); } } } } if let Ok(stats) = server.repo_stats() { let publishers = stats.get_publishers(); res.push('\n'); res.push_str("# HELP krill_repo_publisher number of publishers in repository\n"); res.push_str("# TYPE krill_repo_publisher gauge\n"); res.push_str(&format!("krill_repo_publisher {}\n", publishers.len())); if let Some(last_update) = stats.last_update() { res.push('\n'); res.push_str( "# HELP krill_repo_rrdp_last_update unix timestamp in seconds of last update by any publisher\n", ); res.push_str("# TYPE krill_repo_rrdp_last_update gauge\n"); res.push_str(&format!("krill_repo_rrdp_last_update {}\n", last_update.timestamp())); } res.push('\n'); res.push_str("# HELP krill_repo_rrdp_serial RRDP serial\n"); res.push_str("# TYPE krill_repo_rrdp_serial counter\n"); res.push_str(&format!("krill_repo_rrdp_serial {}\n", stats.serial())); if !server.config.metrics.metrics_hide_publisher_details { res.push('\n'); res.push_str("# HELP krill_repo_objects number of objects in repository for publisher\n"); res.push_str("# TYPE krill_repo_objects gauge\n"); for (publisher, stats) in publishers { res.push_str(&format!( "krill_repo_objects{{publisher=\"{}\"}} {}\n", publisher, stats.objects() )); } res.push('\n'); res.push_str("# HELP krill_repo_size size of objects in bytes in repository for publisher\n"); res.push_str("# TYPE krill_repo_size gauge\n"); for (publisher, stats) in publishers { res.push_str(&format!( "krill_repo_size{{publisher=\"{}\"}} {}\n", publisher, stats.size() )); } res.push('\n'); res.push_str("# HELP krill_repo_last_update unix timestamp in seconds of last update for publisher\n"); res.push_str("# TYPE krill_repo_last_update gauge\n"); for (publisher, stats) in publishers { if let Some(last_update) = stats.last_update() { res.push_str(&format!( "krill_repo_last_update{{publisher=\"{}\"}} {}\n", publisher, last_update.timestamp() )); } } } } Ok(HttpResponse::text(res.into_bytes())) } else { Err(req) } } //------------ Publication --------------------------------------------------- /// Handle RFC8181 queries and return the appropriate response. pub async fn rfc8181(req: Request) -> RoutingResult { if req.path().segment() == "rfc8181" { let mut path = req.path().clone(); let publisher = match path.path_arg() { Some(publisher) => publisher, None => return render_error(Error::ApiInvalidHandle), }; let state = req.state().clone(); let bytes = match req.rfc8181_bytes().await { Ok(bytes) => bytes, Err(e) => return render_error(e), }; match state.rfc8181(publisher, bytes) { Ok(bytes) => Ok(HttpResponse::rfc8181(bytes.to_vec())), Err(e) => render_error(e), } } else { Err(req) } } //------------ Embedded TA -------------------------------------------------- async fn ta(req: Request) -> RoutingResult { match *req.method() { Method::GET => match req.path.full() { "/ta/ta.tal" => tal(req).await, "/testbed.tal" => tal(req).await, "/ta/ta.cer" => ta_cer(req).await, _ => Err(req), }, _ => Err(req), } } pub async fn tal(req: Request) -> RoutingResult { match req.state().ta_cert_details().await { Ok(ta) => Ok(HttpResponse::text(format!("{}", ta.tal()).into_bytes())), Err(_) => render_unknown_resource(), } } pub async fn ta_cer(req: Request) -> RoutingResult { match req.state().trust_anchor_cert().await { Some(cert) => Ok(HttpResponse::cert(cert.to_bytes().to_vec())), None => render_unknown_resource(), } } //------------ Provisioning (RFC6492) ---------------------------------------- /// Process an RFC 6492 request /// pub async fn rfc6492(req: Request) -> RoutingResult { if req.path().segment() == "rfc6492" { let mut path = req.path().clone(); let ca = match path.path_arg() { Some(ca) => ca, None => return render_error(Error::ApiInvalidHandle), }; let actor = req.actor(); let state = req.state().clone(); let user_agent = req.user_agent(); let bytes = match req.rfc6492_bytes().await { Ok(bytes) => bytes, Err(e) => return render_error(e), }; let krill_server = state; match krill_server.rfc6492(ca, bytes, user_agent, &actor).await { Ok(bytes) => Ok(HttpResponse::rfc6492(bytes.to_vec())), Err(e) => render_error(e), } } else { Err(req) } } /// Return various stats as json async fn stats(req: Request) -> RoutingResult { match *req.method() { Method::GET => match req.path().full() { "/stats/info" => render_json(req.state().server_info()), "/stats/repo" => render_json_res(req.state().repo_stats()), "/stats/cas" => render_json_res(req.state().cas_stats().await), _ => Err(req), }, _ => Err(req), } } // Suppress any error in the unlikely event that we fail to inject the // Authorization header into the HTTP response as this is an internal error that // we should shield the user from, but log a warning as this is very unexpected. fn add_authorization_headers_to_response(org_response: HttpResponse, token: Token) -> HttpResponse { let mut new_header_names = Vec::new(); let mut new_header_values = Vec::new(); new_header_names.push(HeaderName::from_str("Authorization")); new_header_values.push(HeaderValue::from_str(&format!("Bearer {}", &token))); let okay = !new_header_names .iter() .zip(new_header_values.iter()) .any(|(n, v)| n.is_err() | v.is_err()); if okay { let (parts, body) = org_response.response().into_parts(); let mut augmented_response = hyper::Response::from_parts(parts, body); let headers = augmented_response.headers_mut(); for (name, value) in new_header_names.into_iter().zip(new_header_values.into_iter()) { headers.insert(name.unwrap(), value.unwrap()); } HttpResponse::new(augmented_response) } else { let mut conversion_errors = Vec::new(); conversion_errors.extend( new_header_names .into_iter() .filter(|result| result.is_err()) .map(|i| i.unwrap_err().to_string()), ); conversion_errors.extend( new_header_values .into_iter() .filter(|result| result.is_err()) .map(|i| i.unwrap_err().to_string()), ); warn!( "Internal error: unable to add refreshed auth token to the response: {:?}", conversion_errors.join(", ") ); org_response } } fn add_new_auth_to_response(res: Result, opt_auth: Option) -> Result { if let Some(Auth::Bearer(token)) = opt_auth { res.map(|ok_res| add_authorization_headers_to_response(ok_res, token)) } else { res } } // aa! macro aka if-authorized-then-run-the-given-code-else-return-http-403 // ------------------------------------------------------------------------ // This macro handles returning from API handler functions if the request is not // Authenticated or lacks sufficient Authorization. We don't use a normal fn for // this as then each API handler function would have to also test for success or // failure and also return the forbidden response to the caller, That would be // both verbose and repetitive. We also can't use the ? operator to return Err // as Err is used to propagate the request to the next handler in the chain. If // we had a child crate we could use a proc macro instead so that we could // "annotate" each API handler function with something like: // #[require_permission(CA_CREATE)] // Which would insert the generated code at the start of the function body, // similar to how this macro is used in each function. macro_rules! aa { (no_warn $req:ident, $perm:expr, $action:expr) => {{ aa!($req, $perm, NO_RESOURCE, $action, true) }}; ($req:ident, $perm:expr, $action:expr) => {{ aa!($req, $perm, NO_RESOURCE, $action, false) }}; (no_warn $req:ident, $perm:expr, $resource:expr, $action:expr) => {{ aa!($req, $perm, $resource, $action, true) }}; ($req:ident, $perm:expr, $resource:expr, $action:expr) => {{ aa!($req, $perm, $resource, $action, false) }}; ($req:ident, $perm:expr, $resource:expr, $action:expr, $benign:expr) => {{ match $req.actor().is_allowed($perm, $resource) { Ok(true) => $action, Ok(false) => { let msg = format!( "User '{}' does not have permission '{}' on resource '{}'", $req.actor().name(), $perm, $resource ); Ok(HttpResponse::forbidden(msg).with_benign($benign)) } Err(err) => { // Avoid an extra round of error -> string -> error conversion // which causes the error message to nest, e.g. // "Invalid credentials: Invalid credentials: Session expired" match err { Error::ApiInvalidCredentials(_) | Error::ApiInsufficientRights(_) | Error::ApiAuthPermanentError(_) | Error::ApiAuthTransientError(_) | Error::ApiAuthSessionExpired(_) | Error::ApiLoginError(_) => Ok(HttpResponse::response_from_error(err).with_benign($benign)), _ => Ok(HttpResponse::forbidden(format!("{}", err)).with_benign($benign)), } } } }}; } /// Maps the API methods async fn api(req: Request) -> RoutingResult { if !req.path().full().starts_with("/api/v1") { Err(req) // Not for us } else { // Eat the first two segments of the path "api/v1" let mut path = req.path().clone(); path.next(); // gets 'v1' and drops it. match path.next() { Some("authorized") => api_authorized(req).await, restricted_endpoint => { // Make sure access is allowed aa!(req, Permission::LOGIN, { match restricted_endpoint { Some("bulk") => api_bulk(req, &mut path).await, Some("cas") => api_cas(req, &mut path).await, Some("pubd") => aa!(req, Permission::PUB_ADMIN, api_publication_server(req, &mut path).await), Some("ta") => aa!(req, Permission::CA_ADMIN, api_ta(req, &mut path).await), _ => render_unknown_method(), } }) } } } } async fn api_authorized(req: Request) -> RoutingResult { // Use 'no_warn' to prevent the log being filled with warnings about // insufficient user rights as this API endpoint is invoked by Lagosta on // every view transition, and not being authorized is a valid state that // triggers Lagosta to show a login form, not something to warn about! aa!(no_warn req, Permission::LOGIN, match *req.method() { Method::GET => render_ok(), _ => render_unknown_method(), } ) } async fn api_bulk(req: Request, path: &mut RequestPath) -> RoutingResult { match path.full() { "/api/v1/bulk/cas/import" => api_cas_import(req).await, "/api/v1/bulk/cas/issues" => api_all_ca_issues(req).await, "/api/v1/bulk/cas/sync/parent" => api_refresh_all(req).await, "/api/v1/bulk/cas/sync/repo" => api_resync_all(req).await, "/api/v1/bulk/cas/publish" => api_republish_all(req, false).await, "/api/v1/bulk/cas/force_publish" => api_republish_all(req, true).await, "/api/v1/bulk/cas/suspend" => api_suspend_all(req).await, _ => render_unknown_method(), } } async fn api_cas(req: Request, path: &mut RequestPath) -> RoutingResult { match path.path_arg::() { Some(ca) => aa!(req, Permission::CA_READ, Handle::from(&ca), { match path.next() { None => match *req.method() { Method::GET => api_ca_info(req, ca).await, Method::DELETE => api_ca_delete(req, ca).await, _ => render_unknown_method(), }, Some("aspas") => api_ca_aspas(req, path, ca).await, Some("bgpsec") => api_ca_bgpsec(req, path, ca).await, Some("children") => api_ca_children(req, path, ca).await, Some("history") => api_ca_history(req, path, ca).await, Some("id") => api_ca_id(req, path, ca).await, Some("issues") => api_ca_issues(req, ca).await, Some("keys") => api_ca_keys(req, path, ca).await, Some("parents") => api_ca_parents(req, path, ca).await, Some("repo") => api_ca_repo(req, path, ca).await, Some("routes") => api_ca_routes(req, path, ca).await, Some("stats") => api_ca_stats(req, path, ca).await, Some("sync") => api_ca_sync(req, path, ca).await, Some("rta") => api_ca_rta(req, path, ca).await, _ => render_unknown_method(), } }), None => match *req.method() { Method::GET => api_cas_list(req).await, Method::POST => api_ca_init(req).await, _ => render_unknown_method(), }, } } async fn api_ca_keys(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match *req.method() { Method::POST => match path.next() { Some("roll_init") => api_ca_kr_init(req, ca).await, Some("roll_activate") => api_ca_kr_activate(req, ca).await, _ => render_unknown_method(), }, _ => render_unknown_method(), } } async fn api_ca_parents(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { if let Some(parent) = path.path_arg() { match *req.method() { Method::GET => api_ca_my_parent_contact(req, ca, parent).await, Method::POST => api_ca_parent_add_or_update(req, ca, Some(parent)).await, Method::DELETE => api_ca_remove_parent(req, ca, parent).await, _ => render_unknown_method(), } } else { match *req.method() { Method::GET => api_ca_my_parent_statuses(req, ca).await, Method::POST => api_ca_parent_add_or_update(req, ca, None).await, _ => render_unknown_method(), } } } async fn api_ca_repo(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.next() { None => match *req.method() { Method::GET => api_ca_repo_details(req, ca).await, Method::POST => api_ca_repo_update(req, ca).await, _ => render_unknown_method(), }, Some("status") => api_ca_repo_status(req, ca).await, _ => render_unknown_method(), } } async fn api_ca_routes(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.next() { None => match *req.method() { Method::GET => api_ca_routes_show(req, ca).await, Method::POST => api_ca_routes_update(req, ca).await, _ => render_unknown_method(), }, Some("try") => match *req.method() { Method::POST => api_ca_routes_try_update(req, ca).await, _ => render_unknown_method(), }, Some("analysis") => api_ca_routes_analysis(req, path, ca).await, _ => render_unknown_method(), } } async fn api_ca_stats(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.next() { Some("children") => match path.next() { Some("connections") => api_ca_stats_child_connections(req, ca).await, _ => render_unknown_method(), }, _ => render_unknown_method(), } } async fn api_ca_sync(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { if req.is_post() { match path.next() { Some("parents") => render_empty_res(req.state().cas_refresh_single(ca).await), Some("repo") => render_empty_res(req.state().cas_repo_sync_single(&ca)), _ => render_unknown_method(), } } else { render_unknown_method() } }) } async fn api_publication_server(req: Request, path: &mut RequestPath) -> RoutingResult { match path.next() { Some("publishers") => api_publishers(req, path).await, Some("delete") => match *req.method() { Method::POST => { let state = req.state.clone(); match req.json().await { Ok(criteria) => render_empty_res(state.delete_matching_files(criteria)), Err(e) => render_error(e), } } _ => render_unknown_method(), }, Some("stale") => api_stale_publishers(req, path.next()).await, Some("init") => match *req.method() { Method::POST => { let state = req.state.clone(); match req.json().await { Ok(uris) => render_empty_res(state.repository_init(uris)), Err(e) => render_error(e), } } Method::DELETE => render_empty_res(req.state.repository_clear()), _ => render_unknown_method(), }, Some("session_reset") => match *req.method() { Method::POST => render_empty_res(req.state().repository_session_reset()), _ => render_unknown_method(), }, _ => render_unknown_method(), } } async fn api_publishers(req: Request, path: &mut RequestPath) -> RoutingResult { match *req.method() { Method::GET => match path.path_arg() { Some(publisher) => match path.next() { None => api_show_pbl(req, publisher).await, Some("response.xml") => api_repository_response_xml(req, publisher).await, Some("response.json") => api_repository_response_json(req, publisher).await, _ => render_unknown_method(), }, None => api_list_pbl(req).await, }, Method::POST => match path.next() { None => api_add_pbl(req).await, _ => render_unknown_method(), }, Method::DELETE => match path.path_arg() { Some(publisher) => api_remove_pbl(req, publisher).await, None => render_error(Error::ApiInvalidHandle), }, _ => render_unknown_method(), } } //------------ Admin: Publishers --------------------------------------------- /// Returns a list of publisher which have not updated for more /// than the given number of seconds. pub async fn api_stale_publishers(req: Request, seconds: Option<&str>) -> RoutingResult { aa!(req, Permission::PUB_LIST, { let seconds = seconds.unwrap_or(""); match i64::from_str(seconds) { Ok(seconds) => render_json_res( req.state() .repo_stats() .map(|stats| PublisherList::build(&stats.stale_publishers(seconds))), ), Err(_) => render_error(Error::ApiInvalidSeconds), } }) } /// Returns a json structure with all publishers in it. pub async fn api_list_pbl(req: Request) -> RoutingResult { aa!(req, Permission::PUB_LIST, { render_json_res( req.state() .publishers() .map(|publishers| PublisherList::build(&publishers)), ) }) } /// Adds a publisher pub async fn api_add_pbl(req: Request) -> RoutingResult { aa!(req, Permission::PUB_CREATE, { let actor = req.actor(); let server = req.state().clone(); match req.json().await { Ok(pbl) => render_json_res(server.add_publisher(pbl, &actor)), Err(e) => render_error(e), } }) } /// Removes a publisher. Should be idempotent! If if did not exist then /// that's just fine. #[allow(clippy::redundant_clone)] // false positive pub async fn api_remove_pbl(req: Request, publisher: PublisherHandle) -> RoutingResult { aa!(req, Permission::PUB_DELETE, { let actor = req.actor(); render_empty_res(req.state().remove_publisher(publisher, &actor)) }) } /// Returns a json structure with publisher details #[allow(clippy::redundant_clone)] // false positive pub async fn api_show_pbl(req: Request, publisher: PublisherHandle) -> RoutingResult { aa!( req, Permission::PUB_READ, render_json_res(req.state().get_publisher(&publisher)) ) } //------------ repository_response --------------------------------------------- #[allow(clippy::redundant_clone)] // false positive pub async fn api_repository_response_xml(req: Request, publisher: PublisherHandle) -> RoutingResult { aa!(req, Permission::PUB_READ, { match repository_response(&req, &publisher).await { Ok(repository_response) => Ok(HttpResponse::xml(repository_response.to_xml_vec())), Err(e) => render_error(e), } }) } #[allow(clippy::redundant_clone)] // false positive pub async fn api_repository_response_json(req: Request, publisher: PublisherHandle) -> RoutingResult { aa!(req, Permission::PUB_READ, { match repository_response(&req, &publisher).await { Ok(res) => render_json(res), Err(e) => render_error(e), } }) } async fn repository_response( req: &Request, publisher: &PublisherHandle, ) -> Result { req.state().repository_response(publisher) } pub async fn api_ca_add_child(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { Ok(child_req) => render_json_res(server.ca_add_child(&ca, child_req, &actor).await), Err(e) => render_error(e), } }) } async fn api_ca_child_update(req: Request, ca: CaHandle, child: ChildHandle) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { Ok(child_req) => render_empty_res(server.ca_child_update(&ca, child, child_req, &actor).await), Err(e) => render_error(e), } }) } pub async fn api_ca_child_remove(req: Request, ca: CaHandle, child: ChildHandle) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_child_remove(&ca, child, &actor).await) }) } async fn api_ca_child_show(req: Request, ca: CaHandle, child: ChildHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_child_show(&ca, &child).await) ) } async fn api_ca_stats_child_connections(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_stats_child_connections(&ca).await) ) } async fn api_ca_parent_res_json(req: Request, ca: CaHandle, child: ChildHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_parent_response(&ca, child.clone()).await) ) } pub async fn api_ca_parent_res_xml(req: Request, ca: CaHandle, child: ChildHandle) -> RoutingResult { aa!(req, Permission::CA_READ, Handle::from(&ca), { match req.state().ca_parent_response(&ca, child.clone()).await { Ok(res) => Ok(HttpResponse::xml(res.to_xml_vec())), Err(e) => render_error(e), } }) } //------------ Admin: CertAuth ----------------------------------------------- async fn api_cas_import(req: Request) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { let server = req.state().clone(); match req.json().await { Ok(structure) => render_empty_res(server.cas_import(structure).await), Err(e) => render_error(e), } }), _ => render_unknown_method(), } } async fn api_all_ca_issues(req: Request) -> RoutingResult { match *req.method() { Method::GET => aa!(req, Permission::CA_READ, { let actor = req.actor(); render_json_res(req.state().all_ca_issues(&actor).await) }), _ => render_unknown_method(), } } /// Returns the health (state) for a given CA. async fn api_ca_issues(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_issues(&ca).await) ), _ => render_unknown_method(), } } async fn api_cas_list(req: Request) -> RoutingResult { aa!(req, Permission::CA_LIST, { let actor = req.actor(); render_json_res(req.state().ca_list(&actor)) }) } pub async fn api_ca_init(req: Request) -> RoutingResult { aa!(req, Permission::CA_CREATE, { let state = req.state().clone(); match req.json().await { Ok(ca_init) => render_empty_res(state.ca_init(ca_init)), Err(e) => render_error(e), } }) } async fn api_ca_id(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_update_id(ca, &actor).await) }), Method::GET => match path.next() { Some("child_request.xml") => api_ca_child_req_xml(req, ca).await, Some("child_request.json") => api_ca_child_req_json(req, ca).await, Some("publisher_request.json") => api_ca_publisher_req_json(req, ca).await, Some("publisher_request.xml") => api_ca_publisher_req_xml(req, ca).await, _ => render_unknown_method(), }, _ => render_unknown_method(), } } async fn api_ca_info(req: Request, handle: CaHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&handle), render_json_res(req.state().ca_info(&handle).await) ) } async fn api_ca_delete(req: Request, handle: CaHandle) -> RoutingResult { let actor = req.actor(); aa!( req, Permission::CA_DELETE, Handle::from(&handle), render_json_res(req.state().ca_delete(&handle, &actor).await) ) } async fn api_ca_my_parent_contact(req: Request, ca: CaHandle, parent: ParentHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_my_parent_contact(&ca, &parent).await) ) } async fn api_ca_my_parent_statuses(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_status(&ca).await.map(|s| s.parents().clone())) ) } async fn api_ca_aspas(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.next() { None => match *req.method() { Method::GET => api_ca_aspas_definitions_show(req, ca).await, Method::POST => api_ca_aspas_definitions_update(req, ca).await, _ => render_unknown_method(), }, // We may need other functions in future, such as 'analyze' or 'try'. // So keep the base namespace clean and use '/api/v1/aspas/as//..' // for functions on specific ASPA definitions for the given (customer) // ASN. Some("as") => { // get as path parameter, or error // - get (specific definition) // - delete // - update? (definition includes the ASN so this can be in the base path) match path.path_arg() { Some(customer) => match *req.method() { Method::POST => api_ca_aspas_update_aspa(req, ca, customer).await, Method::DELETE => api_ca_aspas_delete(req, ca, customer).await, _ => render_unknown_method(), }, None => render_unknown_method(), } } _ => render_unknown_method(), } } async fn api_ca_bgpsec(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { // Handles /api/v1/cas/{ca}/bgpsec/: // GET /api/v1/cas/{ca}/bgpsec/ -> List BGPSec Definitions // POST /api/v1/cas/{ca}/bgpsec/ -> Send BgpSecDefinitionUpdates match path.next() { None => match *req.method() { Method::GET => api_ca_bgpsec_definitions_show(req, ca).await, Method::POST => api_ca_bgpsec_definitions_update(req, ca).await, _ => render_unknown_method(), }, _ => render_unknown_method(), } } async fn api_ca_bgpsec_definitions_show(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::BGPSEC_READ, Handle::from(&ca), { render_json_res(req.state().ca_bgpsec_definitions_show(ca).await) }) } async fn api_ca_bgpsec_definitions_update(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::BGPSEC_UPDATE, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { Ok(updates) => render_empty_res(server.ca_bgpsec_definitions_update(ca, updates, &actor).await), Err(e) => render_error(e), } }) } async fn api_ca_children(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.path_arg() { Some(child) => match path.next() { None => match *req.method() { Method::GET => api_ca_child_show(req, ca, child).await, Method::POST => api_ca_child_update(req, ca, child).await, Method::DELETE => api_ca_child_remove(req, ca, child).await, _ => render_unknown_method(), }, Some("contact") | Some("parent_response.json") => api_ca_parent_res_json(req, ca, child).await, Some("parent_response.xml") => api_ca_parent_res_xml(req, ca, child).await, _ => render_unknown_method(), }, None => match *req.method() { Method::POST => api_ca_add_child(req, ca).await, _ => render_unknown_method(), }, } } async fn api_ca_history_commands(req: Request, path: &mut RequestPath, handle: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!(req, Permission::CA_READ, Handle::from(&handle), { // /api/v1/cas/{ca}/history/commands //// let mut crit = CommandHistoryCriteria::default(); if let Some(rows) = path.path_arg() { crit.set_rows(rows); } if let Some(offset) = path.path_arg() { crit.set_offset(offset); } if let Some(after) = path.path_arg() { crit.set_after(after); } if let Some(before) = path.path_arg() { crit.set_before(before); } match req.state().ca_history(&handle, crit).await { Ok(history) => render_json(history), Err(e) => render_error(e), } }), _ => render_unknown_method(), } } async fn api_ca_history(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.next() { Some("details") => api_ca_command_details(req, path, ca).await, Some("commands") => api_ca_history_commands(req, path, ca).await, _ => render_unknown_method(), } } #[allow(clippy::redundant_clone)] // false positive async fn api_ca_command_details(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { // /api/v1/cas/{ca}/command/ match path.path_arg() { Some(key) => match *req.method() { Method::GET => aa!(req, Permission::CA_READ, Handle::from(&ca), { match req.state().ca_command_details(&ca, key) { Ok(details) => render_json(details), Err(e) => match e { Error::AggregateStoreError(AggregateStoreError::UnknownCommand(_, _)) => { render_unknown_resource() } _ => render_error(e), }, } }), _ => render_unknown_method(), }, None => render_unknown_resource(), } } async fn api_ca_child_req_xml(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, Permission::CA_READ, Handle::from(&ca), match ca_child_req(&req, &ca).await { Ok(child_request) => Ok(HttpResponse::xml(child_request.to_xml_vec())), Err(e) => render_error(e), } ), _ => render_unknown_method(), } } async fn api_ca_child_req_json(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, Permission::CA_READ, Handle::from(&ca), match ca_child_req(&req, &ca).await { Ok(req) => render_json(req), Err(e) => render_error(e), } ), _ => render_unknown_method(), } } async fn ca_child_req(req: &Request, ca: &CaHandle) -> Result { req.state().ca_child_req(ca).await } async fn api_ca_publisher_req_json(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_publisher_req(&ca).await) ), _ => render_unknown_method(), } } async fn api_ca_publisher_req_xml(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, Permission::CA_READ, Handle::from(&ca), match req.state().ca_publisher_req(&ca).await { Ok(publisher_request) => Ok(HttpResponse::xml(publisher_request.to_xml_vec())), Err(e) => render_error(e), } ), _ => render_unknown_method(), } } async fn api_ca_repo_details(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_repo_details(&ca).await) ) } async fn api_ca_repo_status(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, Permission::CA_READ, Handle::from(&ca), render_json_res(req.state().ca_status(&ca).await.map(|status| status.repo().clone())) ), _ => render_unknown_method(), } } fn extract_repository_contact(ca: &CaHandle, bytes: Bytes) -> Result { let string = String::from_utf8(bytes.to_vec()).map_err(Error::custom)?; // Get rid of whitespace first so we can check if it smells like XML. // We could change this to check for Content-Type headers instead. let string = string.trim(); if string.starts_with('<') { if string.contains(" RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req .api_bytes() .await .map(|bytes| extract_repository_contact(&ca, bytes)) { Ok(Ok(update)) => render_empty_res(server.ca_repo_update(ca, update, &actor).await), Ok(Err(e)) | Err(e) => render_error(e), } }) } async fn api_ca_parent_add_or_update( req: Request, ca: CaHandle, parent_override: Option, ) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); let bytes = match req.api_bytes().await { Ok(bytes) => bytes, Err(e) => return render_error(e), }; match extract_parent_ca_req(&ca, bytes, parent_override) { Ok(parent_req) => render_empty_res(server.ca_parent_add_or_update(ca, parent_req, &actor).await), Err(e) => render_error(e), } }) } fn extract_parent_ca_req( ca: &CaHandle, bytes: Bytes, parent_override: Option, ) -> Result { let string = String::from_utf8(bytes.to_vec()).map_err(Error::custom)?; // Get rid of whitespace first so we can check if it smells like XML. // We could change this to check for Content-Type headers instead. let string = string.trim(); let req = if string.starts_with('<') { if string.starts_with(" RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_parent_remove(ca, parent, &actor).await) }) } /// Force a key roll for a CA, i.e. use a max key age of 0 seconds. async fn api_ca_kr_init(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_keyroll_init(ca, &actor).await) }) } /// Force key activation for all new keys, i.e. use a staging period of 0 seconds. async fn api_ca_kr_activate(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_keyroll_activate(ca, &actor).await) }) } // -- ASPA functions /// List the current ASPA definitions for a CA async fn api_ca_aspas_definitions_show(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::ASPAS_READ, Handle::from(&ca), { let state = req.state().clone(); render_json_res(state.ca_aspas_definitions_show(ca).await) }) } /// Add a new ASPA definition for a CA based on the update in the POST async fn api_ca_aspas_definitions_update(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::ASPAS_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { Err(e) => render_error(e), Ok(updates) => render_empty_res(state.ca_aspas_definitions_update(ca, updates, &actor).await), } }) } /// Update an existing ASPA definition for a CA based on the update in the POST async fn api_ca_aspas_update_aspa(req: Request, ca: CaHandle, customer: Asn) -> RoutingResult { aa!(req, Permission::ASPAS_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { Err(e) => render_error(e), Ok(update) => render_empty_res(state.ca_aspas_update_aspa(ca, customer, update, &actor).await), } }) } /// Delete the ASPA definition for the given CA and customer ASN async fn api_ca_aspas_delete(req: Request, ca: CaHandle, customer: Asn) -> RoutingResult { aa!(req, Permission::ASPAS_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); let updates = AspaDefinitionUpdates::new(vec![], vec![customer]); render_empty_res(state.ca_aspas_definitions_update(ca, updates, &actor).await) }) } /// Update the route authorizations for this CA async fn api_ca_routes_update(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::ROUTES_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { Err(e) => render_error(e), Ok(updates) => render_empty_res(state.ca_routes_update(ca, updates, &actor).await), } }) } /// Tries an update. If the dry-run for it would be successful, and the analysis /// for the resources in the update have no remaining invalids, apply it. Otherwise /// return the analysis and a suggestion. async fn api_ca_routes_try_update(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::ROUTES_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json::().await { Err(e) => render_error(e), Ok(updates) => { let server = state; match server.ca_routes_bgp_dry_run(&ca, updates.clone()).await { Err(e) => { // update was rejected, return error render_error(e) } Ok(effect) => { if !effect.contains_invalids() { // no issues found, apply render_empty_res(server.ca_routes_update(ca, updates, &actor).await) } else { // remaining invalids exist, advise user let updates = updates.into_explicit_max_length(); let resources = updates.affected_prefixes(); match server.ca_routes_bgp_suggest(&ca, Some(resources)).await { Err(e) => render_error(e), // should not fail after dry run, but hey.. Ok(suggestion) => render_json(BgpAnalysisAdvice::new(effect, suggestion)), } } } } } } }) } /// show the route authorizations for this CA async fn api_ca_routes_show(req: Request, ca: CaHandle) -> RoutingResult { aa!(req, Permission::ROUTES_READ, Handle::from(&ca), { match req.state().ca_routes_show(&ca).await { Ok(roas) => render_json(roas), Err(_) => render_unknown_resource(), } }) } /// Show the state of ROAs vs BGP for this CA async fn api_ca_routes_analysis(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { aa!(req, Permission::ROUTES_ANALYSIS, Handle::from(&ca), { match path.next() { Some("full") => render_json_res(req.state().ca_routes_bgp_analysis(&ca).await), Some("dryrun") => match *req.method() { Method::POST => { let state = req.state.clone(); match req.json().await { Err(e) => render_error(e), Ok(updates) => render_json_res(state.ca_routes_bgp_dry_run(&ca, updates).await), } } _ => render_unknown_method(), }, Some("suggest") => match *req.method() { Method::GET => render_json_res(req.state().ca_routes_bgp_suggest(&ca, None).await), Method::POST => { let server = req.state().clone(); match req.json().await { Err(e) => render_error(e), Ok(resources) => render_json_res(server.ca_routes_bgp_suggest(&ca, Some(resources)).await), } } _ => render_unknown_method(), }, _ => render_unknown_method(), } }) } //------------ Admin: Force republish ---------------------------------------- async fn api_republish_all(req: Request, force: bool) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { render_empty_res(req.state().republish_all(force).await) }), _ => render_unknown_method(), } } async fn api_resync_all(req: Request) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { let actor = req.actor(); render_empty_res(req.state().cas_repo_sync_all(&actor)) }), _ => render_unknown_method(), } } /// Refresh all CAs async fn api_refresh_all(req: Request) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { render_empty_res(req.state().cas_refresh_all().await) }), _ => render_unknown_method(), } } /// Schedule check suspend for all CAs async fn api_suspend_all(req: Request) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { render_empty_res(req.state().cas_schedule_suspend_all()) }), _ => render_unknown_method(), } } //------------ Serve RRDP Files ---------------------------------------------- async fn rrdp(req: Request) -> RoutingResult { if !req.path().full().starts_with("/rrdp/") { Err(req) // Not for us } else { let mut full_path: PathBuf = req.state.rrdp_base_path(); let (_, path) = req.path.remaining().split_at(1); let cache_seconds = if path.ends_with("notification.xml") { 60 } else { 86400 }; full_path.push(path); match File::open(&full_path) { Ok(mut file) if full_path.is_file() => { let mut buffer = Vec::new(); match file.read_to_end(&mut buffer) { Ok(_) => Ok(HttpResponse::xml_with_cache(buffer, cache_seconds)), Err(_) => Ok(HttpResponse::not_found()), } } _ => Ok(HttpResponse::not_found()), } } } //------------ Support Resource Tagged Attestations (RTA) ---------------------- async fn api_ca_rta(req: Request, path: &mut RequestPath, ca: CaHandle) -> RoutingResult { match path.path_arg() { Some(name) => match *req.method() { Method::POST => match path.next() { Some("sign") => api_ca_rta_sign(req, ca, name).await, Some("multi") => match path.next() { Some("prep") => api_ca_rta_multi_prep(req, ca, name).await, Some("cosign") => api_ca_rta_multi_sign(req, ca, name).await, _ => render_unknown_method(), }, _ => render_unknown_method(), }, Method::GET => { if name.is_empty() { api_ca_rta_list(req, ca).await } else { api_ca_rta_show(req, ca, name).await } } _ => render_unknown_method(), }, None => match *req.method() { Method::GET => api_ca_rta_list(req, ca).await, _ => render_unknown_method(), }, } } async fn api_ca_rta_list(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, Permission::RTA_LIST, Handle::from(&ca), render_json_res(req.state().rta_list(ca).await) ) } async fn api_ca_rta_show(req: Request, ca: CaHandle, name: RtaName) -> RoutingResult { aa!( req, Permission::RTA_READ, Handle::from(&ca), render_json_res(req.state().rta_show(ca, name).await) ) } async fn api_ca_rta_sign(req: Request, ca: CaHandle, name: RtaName) -> RoutingResult { aa!(req, Permission::RTA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { Err(e) => render_error(e), Ok(request) => render_empty_res(state.rta_sign(ca, name, request, &actor).await), } }) } async fn api_ca_rta_multi_prep(req: Request, ca: CaHandle, name: RtaName) -> RoutingResult { aa!(req, Permission::RTA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { Ok(resources) => render_json_res(state.rta_multi_prep(ca, name, resources, &actor).await), Err(e) => render_error(e), } }) } async fn api_ca_rta_multi_sign(req: Request, ca: CaHandle, name: RtaName) -> RoutingResult { aa!(req, Permission::RTA_UPDATE, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { Ok(rta) => render_empty_res(state.rta_multi_cosign(ca, name, rta, &actor).await), Err(_) => render_error(Error::custom("Cannot decode RTA for co-signing")), } }) } //-------------------------------- API TA -------------------------------------------------- async fn api_ta(req: Request, path: &mut RequestPath) -> RoutingResult { // // krillta proxy --server .. --token .. // // Uses API krill API: // // /api/v1/ta/ // // - proxy and signer set up // POST /proxy/init initialise proxy // POST /proxy/id proxy id cert info // GET /proxy/repo/request.xml get RFC8181 publisher request // GET /proxy/repo/request.json get RFC8181 publisher request // GET /proxy/repo get repository contact // POST /proxy/repo add pub server // POST /proxy/signer/add add initialised signer to proxy // POST /proxy/signer/request create sign request for signer (returns request) // GET /proxy/signer/request show open sign request if any // POST /proxy/signer/response process sign response from signer // // - children // GET /proxy/children/ future: list children // POST /proxy/children/ add child // GET /proxy/children/{child}/parent_response.json show parent response for child // GET /proxy/children/{child}/parent_response.xml show parent response for child // POST /proxy/children/{child} future: update child // DEL /proxy/children/{child} future: remove child // // krillta signer --dir // init // process // history (future) // response match path.next() { Some("proxy") => match path.next() { Some("init") => render_empty_res(req.state().ta_proxy_init().await), Some("id") => render_json_res(req.state().ta_proxy_id().await), Some("repo") => match path.next() { Some("request.xml") => match req.state().ta_proxy_publisher_request().await { Ok(req) => Ok(HttpResponse::xml(req.to_xml_vec())), Err(e) => render_error(e), }, Some("request.json") => render_json_res(req.state().ta_proxy_publisher_request().await), None => match *req.method() { Method::POST => { let ta_handle = ta::ta_handle(); let server = req.state().clone(); let actor = req.actor.clone(); match req .api_bytes() .await .map(|bytes| extract_repository_contact(&ta_handle, bytes)) { Ok(Ok(contact)) => { render_empty_res(server.ta_proxy_repository_update(contact, &actor).await) } Ok(Err(e)) | Err(e) => render_error(e), } } Method::GET => render_json_res(req.state().ta_proxy_repository_contact().await), _ => render_unknown_method(), }, _ => render_unknown_method(), }, Some("signer") => match path.next() { Some("add") => { let server = req.state().clone(); let actor = req.actor.clone(); match req.json().await { Ok(ta_signer_info) => { render_empty_res(server.ta_proxy_signer_add(ta_signer_info, &actor).await) } Err(e) => render_error(e), } } Some("request") => match *req.method() { Method::POST => render_json_res(req.state().ta_proxy_signer_make_request(&req.actor()).await), Method::GET => render_json_res(req.state().ta_proxy_signer_get_request().await), _ => render_unknown_method(), }, Some("response") => match *req.method() { Method::POST => { let server = req.state().clone(); let actor = req.actor.clone(); match req.json().await { Ok(response) => { render_empty_res(server.ta_proxy_signer_process_response(response, &actor).await) } Err(e) => render_error(e), } } _ => render_unknown_method(), }, _ => render_unknown_method(), }, Some("children") => match path.path_arg::() { Some(child) => match path.next() { Some("parent_response.json") => { render_json_res(req.state().ca_parent_response(&ta::ta_handle(), child).await) } Some("parent_response.xml") => { match req.state().ca_parent_response(&ta::ta_handle(), child).await { Ok(parent_response) => Ok(HttpResponse::xml(parent_response.to_xml_vec())), Err(e) => render_error(e), } } None => match *req.method() { Method::POST => render_error(Error::custom("update TA child not yet supported")), Method::DELETE => render_error(Error::custom("remove TA child not yet supported")), _ => render_unknown_method(), }, _ => render_unknown_method(), }, None => match *req.method() { Method::POST => { let actor = req.actor(); let server = req.state().clone(); match req.json().await { Ok(child_req) => render_json_res(server.ta_proxy_children_add(child_req, &actor).await), Err(e) => render_error(e), } } Method::GET => render_error(Error::custom("show TA child not yet supported")), _ => render_unknown_method(), }, }, _ => render_unknown_method(), }, _ => render_unknown_method(), } } /// A naive lock implementation used to prevent that two Krill instances /// access the same krill data directory simultaneously. struct KrillLock(PathBuf); impl KrillLock { fn create(config: &Config) -> Self { let lock_file_path = config.data_dir.join("krill.lock"); if lock_file_path.exists() { error!( "Cannot start Krill: existing lock file found at: {}", lock_file_path.display() ); ::std::process::exit(1); } if let Err(e) = file::save(b"lock", &lock_file_path) { error!( "Cannot start Krill: cannot create lock file at: {}. Error: {}", lock_file_path.display(), e ); ::std::process::exit(1); } KrillLock(lock_file_path) } fn clean(&self) { // best effort clean up let _ = std::fs::remove_file(&self.0); } async fn handle_ctrl_c(&self) { tokio::signal::ctrl_c().await.unwrap(); self.clean(); } #[cfg(unix)] async fn handle_sig_term(&self) { tokio::signal::unix::signal(SignalKind::terminate()) .unwrap() .recv() .await; self.clean(); } } impl Drop for KrillLock { fn drop(&mut self) { self.clean() } } //------------ Tests --------------------------------------------------------- #[cfg(test)] mod tests { // NOTE: This is extensively tested through the functional and e2e tests found under // the $project/tests dir use crate::test; use std::fs; #[tokio::test] async fn start_krill_daemon() { let dir = test::start_krill_with_default_test_config(false, false, false, false).await; let _ = fs::remove_dir_all(dir); } #[tokio::test] async fn start_krill_pubd_daemon() { let dir = test::start_krill_pubd(0).await; let _ = fs::remove_dir_all(dir); } }