diff --git a/TODO.md b/TODO.md index d57d81ac..c5287299 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,3 @@ -As part of this PR: - * command details history output has changed. Check and document. * Krill internally always stores ROA payload with an explicit max length. Enforce this through a special type. (Make sure to be lenient when @@ -8,9 +6,6 @@ As part of this PR: * `BgpAnalysisEntry` contains a cloned `ConfiguredRoa`. Maybe it can contain a ref or a cow? Also, this should probably be switched into an enum to avoid `configured_roa? and `announcement` to panic. - -Follow-up: - * Fix `impl Hash for crate::commons::api::admin::RepositoryContact`. (This will require a few changes in `crate::deamon::ca`.) * `commons::api::ca::ParentStatuses::sync_candidates` can re-use the @@ -31,8 +26,16 @@ Follow-up: * Use Cows in API structs to avoid cloning on the server side. This will also allow removing quite a few temporary vecs and replace them with iterators. +* /metrics and /stats are completely open. That sounds a bit dangerous. +* Change HTTP responses so we can stream a file, then change the /rrdp + endpoint to stream the file. Notes * I think we should apply API calls that create multiple commands atomically. + +Tests + +* Check the cache headers on RRDP responses. + diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..0c9ca3c4 --- /dev/null +++ b/build.rs @@ -0,0 +1,175 @@ +//! Build script. +//! +//! This script collects the assets for serving the Krill UI and creates +//! a module for them in `$OUT_DIR/ui_assets.rs`. +use std::{env, fmt, fs, io, process}; +use std::path::{PathBuf, Path}; + +const UI_DIR: &str = "ui"; +const INDEX_PATH: &str = "ui/index.html"; +const ASSETS_DIR: &str = "ui/assets"; +const RS_FILE: &str = "ui_assets.rs"; + +const TYPES: &[(&str, &str)] = &[ + ("css", "text/css"), + ("html", "text/html"), + ("ico", "image/x-icon"), + ("js", "text/javascript"), + ("svg", "image/svg+xml"), + ("woff", "font/woff"), + ("woff2", "font/woff2"), +]; + +struct Asset { + path: PathBuf, + media_type: &'static str, + content: Vec, +} + +impl Asset { + fn load(path: PathBuf, asset: bool) -> Result { + let path_ext = match path.extension().and_then(|s| s.to_str()) { + Some(ext) => ext, + None => { + return Err(format!( + "Asset without extension: '{}'", path.display() + )) + } + }; + + let media_type = match TYPES.iter().find_map(|(ext, media_type)| { + (path_ext == *ext).then_some(*media_type) + }) { + Some(media) => media, + None => { + return Err(format!( + "Asset with unknown extension '{}'", path_ext + )) + } + }; + + Ok(Self { + path: if asset { + path.strip_prefix(ASSETS_DIR).map_err(|_| { + format!("Asset path {} not under {}", + path.display(), ASSETS_DIR + ) + })?.into() + } + else { + INDEX_PATH.into() + }, + media_type, + content: fs::read(&path).map_err(|err| { + format!( + "Failed to read UI asset file {}: {}.", + path.display(), err + ) + })? + }) + } +} + +impl fmt::Display for Asset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + " + Asset {{ + path: r#\"{}\"#, + media_type: \"{}\", + content: &{:?}, + }} + ", + self.path.display(), + self.media_type, + self.content.as_slice(), + ) + } +} + + +#[derive(Default)] +struct Assets(Vec); + +impl Assets { + fn load_dir(&mut self, path: PathBuf) -> Result<(), String> { + let dir = fs::read_dir(&path).map_err(|err| { + format!("Failed to open directory {}: {}", path.display(), err) + })?; + for entry in dir { + let entry = entry.map_err(|err| { + format!("Failed to read directory {}: {}", path.display(), err) + })?; + let path = entry.path(); + if path.is_dir() { + self.load_dir(path)?; + } + else { + self.0.push(Asset::load(path, true)?) + } + } + Ok(()) + } +} + + +fn write_mod( + index: Asset, assets: Assets, dest: &mut impl io::Write +) -> Result<(), io::Error> { + write!(dest, + r#" + pub struct Asset {{ + pub path: &'static str, + pub media_type: &'static str, + pub content: &'static [u8], + }} + + pub static INDEX: Asset = {index}; + + pub static ASSETS: &[Asset] = &[ + "# + )?; + for item in assets.0 { + write!(dest, "{},", item)?; + } + writeln!(dest, "];") +} + + +fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap_or_default(); + let target_path = Path::new(&out_dir).join(RS_FILE); + let mut target = match fs::File::create(&target_path) { + Ok(target) => io::BufWriter::new(target), + Err(err) => { + eprintln!("Failed to open assets module file {}: {}", + target_path.display(), err + ); + process::exit(1); + } + }; + + let index = match Asset::load(INDEX_PATH.into(), false) { + Ok(index) => index, + Err(err) => { + eprintln!("{}", err); + process::exit(1); + } + }; + + let mut assets = Assets::default(); + if let Err(err) = assets.load_dir(ASSETS_DIR.into()) { + eprintln!("{}", err); + process::exit(1); + } + + if let Err(err) = write_mod(index, assets, &mut target) { + eprintln!("Failed to write to assets module file {}: {}", + target_path.display(), err + ); + process::exit(1) + } + + println!("cargo:rerun-if-changed={}", UI_DIR); +} + diff --git a/src/api/admin.rs b/src/api/admin.rs index 05abebcc..ab8c7cb3 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -72,7 +72,7 @@ pub struct PublisherSummary { } impl PublisherSummary { - fn from_handle(handle: PublisherHandle) -> Self { + pub fn from_handle(handle: PublisherHandle) -> Self { PublisherSummary { handle } } } diff --git a/src/api/pubd.rs b/src/api/pubd.rs index 4f707947..6435b94e 100644 --- a/src/api/pubd.rs +++ b/src/api/pubd.rs @@ -20,20 +20,23 @@ pub struct RepoStats { } impl RepoStats { - pub fn stale_publishers(&self, seconds: i64) -> Vec { - let mut res = vec![]; - for (publisher, stats) in self.publishers.iter() { + pub fn stale_publishers( + self, seconds: i64 + ) -> impl Iterator { + self.publishers.into_iter().filter_map(move |(publisher, stats)| { if let Some(update_time) = stats.last_update() { if Time::now().timestamp() - update_time.timestamp() >= seconds { - res.push(publisher.clone()) + Some(publisher) + } + else { + None } } else { - res.push(publisher.clone()) + Some(publisher) } - } - res + }) } } diff --git a/src/api/status.rs b/src/api/status.rs index 5d8d246b..5b660aca 100644 --- a/src/api/status.rs +++ b/src/api/status.rs @@ -71,7 +71,7 @@ impl ErrorResponse { } } - fn with_arg(mut self, key: &str, value: impl fmt::Display) -> Self { + pub fn with_arg(mut self, key: &str, value: impl fmt::Display) -> Self { self.args.insert(key.to_string(), value.to_string()); self } diff --git a/src/api/ta.rs b/src/api/ta.rs index 1cedbfe9..d10ffe65 100644 --- a/src/api/ta.rs +++ b/src/api/ta.rs @@ -274,39 +274,10 @@ impl fmt::Display for TrustAnchorObjects { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TaCertDetails { - cert: ReceivedCert, - tal: TrustAnchorLocator, + pub cert: ReceivedCert, + pub tal: TrustAnchorLocator, } -impl TaCertDetails { - pub fn new(cert: ReceivedCert, tal: TrustAnchorLocator) -> Self { - TaCertDetails { cert, tal } - } - - pub fn cert(&self) -> &ReceivedCert { - &self.cert - } - - pub fn resources(&self) -> &ResourceSet { - &self.cert.resources - } - - pub fn tal(&self) -> &TrustAnchorLocator { - &self.tal - } -} - -impl From for ReceivedCert { - fn from(details: TaCertDetails) -> Self { - details.cert - } -} - -impl From for TrustAnchorLocator { - fn from(details: TaCertDetails) -> Self { - details.tal - } -} //------------ TrustAnchorLocator -------------------------------------------- @@ -411,7 +382,7 @@ impl fmt::Display for TrustAnchorSignerInfo { "-------------------------------------------------------" )?; writeln!(f)?; - writeln!(f, "{}", self.ta_cert_details.tal())?; + writeln!(f, "{}", self.ta_cert_details.tal)?; writeln!(f)?; writeln!( f, diff --git a/src/bin/krill.rs b/src/bin/krill.rs index 23f930e6..e0795a66 100644 --- a/src/bin/krill.rs +++ b/src/bin/krill.rs @@ -6,8 +6,8 @@ use clap::Parser; use clap::crate_version; use log::error; use krill::constants::{KRILL_DEFAULT_CONFIG_FILE, KRILL_SERVER_APP}; -use krill::server::config::Config; -use krill::server::http::server; +use krill::config::Config; +use krill::daemon::start::start_krill_daemon; //------------ main ---------------------------------------------------------- @@ -18,7 +18,7 @@ async fn main() { match Config::create(&args.config, false) { Ok(config) => { - if let Err(e) = server::start_krill_daemon( + if let Err(e) = start_krill_daemon( Arc::new(config), None ).await { error!("Krill failed to start: {}", e); diff --git a/src/bin/krillup.rs b/src/bin/krillup.rs index 03fc9498..9d7642e7 100644 --- a/src/bin/krillup.rs +++ b/src/bin/krillup.rs @@ -7,7 +7,7 @@ use log::info; use log::LevelFilter; use url::Url; use krill::constants; -use krill::server::config::{Config, LogType}; +use krill::config::{Config, LogType}; use krill::server::properties::PropertiesManager; use krill::upgrades::{prepare_upgrade_data_migrations, UpgradeMode}; use krill::upgrades::data_migration::migrate; diff --git a/src/commons/crypto/signing/dispatch/krillsigner.rs b/src/commons/crypto/signing/dispatch/krillsigner.rs index 8f5877b1..4d5a161e 100644 --- a/src/commons/crypto/signing/dispatch/krillsigner.rs +++ b/src/commons/crypto/signing/dispatch/krillsigner.rs @@ -43,7 +43,7 @@ use crate::{ KrillResult, }, constants::ID_CERTIFICATE_VALIDITY_YEARS, - server::config::{SignerConfig, SignerType}, + config::{SignerConfig, SignerType}, }; use crate::api::ca::ObjectName; @@ -517,7 +517,7 @@ pub mod tests { MockSigner, MockSignerCallCounts, }, commons::test, - server::config::Config, + config::Config, }; use super::*; diff --git a/src/commons/error.rs b/src/commons/error.rs index 4abcc1a0..a61b37e0 100644 --- a/src/commons/error.rs +++ b/src/commons/error.rs @@ -1,6 +1,6 @@ //! Defines all Krill server side errors -use std::{fmt, fmt::Display, io}; +use std::{error, fmt, fmt::Display, io}; use hyper::StatusCode; @@ -29,8 +29,8 @@ use crate::{ storage, storage::KeyValueError, }, - server::http::tls_keys, - server::http::auth::Permission, + daemon::http::tls_keys, + daemon::http::auth::Permission, server::pubd::PublicationDeltaError, upgrades::UpgradeError, }; @@ -212,6 +212,8 @@ impl fmt::Display for FatalError { } } +impl error::Error for FatalError { } + //------------ Error ------------------------------------------------------- // Transitional type alias. @@ -233,10 +235,12 @@ pub enum Error { HttpClientError(httpclient::Error), ConfigError(String), UpgradeError(UpgradeError), + NotImplemented, //----------------------------------------------------------------- // General API Client Issues //----------------------------------------------------------------- + UnexpectedBody, JsonError(serde_json::Error), InvalidUtf8Input, ApiUnknownMethod, @@ -318,6 +322,7 @@ pub enum Error { //----------------------------------------------------------------- // CA Child Issues //----------------------------------------------------------------- + CaChildImportHandleMismatch { path: ChildHandle, body: ChildHandle }, CaChildDuplicate(CaHandle, ChildHandle), CaChildUnknown(CaHandle, ChildHandle), CaChildMustHaveResources(CaHandle, ChildHandle), @@ -413,10 +418,12 @@ impl fmt::Display for Error { Error::HttpClientError(e) => write!(f, "HTTP client error: {}", e), Error::ConfigError(e) => write!(f, "Configuration error: {}", e), Error::UpgradeError(e) => write!(f, "Could not upgrade Krill: {}", e), + Error::NotImplemented => write!(f, "Not yet implemented"), //----------------------------------------------------------------- // General API Client Issues //----------------------------------------------------------------- + Error::UnexpectedBody => write!(f, "Unexpected body in request"), Error::JsonError(e) => write!(f,"Invalid JSON: {}", e), Error::InvalidUtf8Input => write!(f, "Submitted bytes are invalid UTF8"), Error::ApiUnknownMethod => write!(f,"Unknown API method"), @@ -510,6 +517,12 @@ impl fmt::Display for Error { //----------------------------------------------------------------- // CA Child Issues //----------------------------------------------------------------- + Error::CaChildImportHandleMismatch { path, body } => { + write!(f, + "mismatch between child handles: \ + '{path}' in path, '{body}' in body" + ) + }, Error::CaChildDuplicate(ca, child) => write!(f, "CA '{}' already has a child named '{}'", ca, child), Error::CaChildUnknown(ca, child) => write!(f, "CA '{}' does not have a child named '{}'", ca, child), Error::CaChildMustHaveResources(ca, child) => write!(f, "Child '{}' for CA '{}' MUST have resources specified", child, ca), @@ -773,6 +786,8 @@ impl Error { | Error::ApiLoginError(_) => StatusCode::UNAUTHORIZED, Error::ApiInsufficientRights(_) => StatusCode::FORBIDDEN, + Error::NotImplemented => StatusCode::NOT_IMPLEMENTED, + _ => StatusCode::BAD_REQUEST, } } @@ -833,9 +848,18 @@ impl Error { ErrorResponse::new("sys-upgrade", self).with_cause(e) } + // not yet implemented error + Error::NotImplemented => { + ErrorResponse::new("sys-not-implemented", self) + } + //----------------------------------------------------------------- // General API Client Issues (label: api-*) //----------------------------------------------------------------- + Error::UnexpectedBody => { + ErrorResponse::new("api-unexpected-body", self) + } + Error::JsonError(e) => { ErrorResponse::new("api-json", self).with_cause(e) } @@ -1064,6 +1088,11 @@ impl Error { } // CA Child Issues + Error::CaChildImportHandleMismatch { path, body } => { + ErrorResponse::new("ca-child-import-handle-mismatch", self) + .with_arg("path", path) + .with_arg("body", body) + } Error::CaChildDuplicate(ca, child) => { ErrorResponse::new("ca-child-duplicate", self) .with_ca(ca) diff --git a/src/server/config.rs b/src/config.rs similarity index 99% rename from src/server/config.rs rename to src/config.rs index d60fee70..8272999f 100644 --- a/src/server/config.rs +++ b/src/config.rs @@ -31,17 +31,17 @@ use crate::{ KrillResult, }, constants::*, - server::{ + daemon::{ http::auth::{Role, RoleMap}, http::tls_keys::{self, HTTPS_SUB_DIR}, - mq::{in_seconds, Priority}, }, + server::mq::{in_seconds, Priority}, tasigner::TaTimingConfig, }; use crate::api::admin::{PublicationServerUris, Token}; #[cfg(feature = "multi-user")] -use crate::server::http::auth::providers::{ +use crate::daemon::http::auth::providers::{ config_file::ConfigAuthUsers, openid_connect::ConfigAuthOpenIDConnect, }; diff --git a/src/constants.rs b/src/constants.rs index ed6493c3..38989f06 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -277,6 +277,18 @@ pub fn ta_resource_class_name() -> rpki::ca::provisioning::ResourceClassName { } +//------------ Testbed ------------------------------------------------------- + +/// The handle of the CA used by the testbed. +pub const TESTBED_CA_NAME: &str = "testbed"; + +/// Returns the CA handle for the testbed. +pub fn testbed_ca_handle() -> CaHandle { + use std::str::FromStr; + CaHandle::from_str(TESTBED_CA_NAME).unwrap() +} + + //------------ Config File Auth Provider Defaults ---------------------------- // // Note: These must match the values used by Lagosta. diff --git a/src/server/http/auth/authorizer.rs b/src/daemon/http/auth/authorizer.rs similarity index 93% rename from src/server/http/auth/authorizer.rs rename to src/daemon/http/auth/authorizer.rs index e61480e1..710218e6 100644 --- a/src/server/http/auth/authorizer.rs +++ b/src/daemon/http/auth/authorizer.rs @@ -9,9 +9,9 @@ use crate::api::admin::Token; use crate::commons::KrillResult; use crate::commons::actor::Actor; use crate::commons::error::ApiAuthError; -use crate::server::config::{AuthType, Config}; -use crate::server::http::request::HyperRequest; -use crate::server::http::response::HttpResponse; +use crate::config::{AuthType, Config}; +use crate::daemon::http::request::HyperRequest; +use crate::daemon::http::response::HttpResponse; use super::{Permission, Role}; use super::providers::admin_token; #[cfg(feature = "multi-user")] @@ -80,7 +80,7 @@ impl AuthProvider { pub async fn authenticate( &self, request: &HyperRequest, - ) -> Result, ApiAuthError> { + ) -> Result)>, ApiAuthError> { match &self { AuthProvider::Token(provider) => provider.authenticate(request), #[cfg(feature = "multi-user")] @@ -159,6 +159,7 @@ impl AuthProvider { } /// If necessary, spawns a Tokio task sweeping the session cache. + #[allow(unused_variables)] pub fn spawn_sweep(&self, runtime: &runtime::Handle) { match self { AuthProvider::Token(_) => { } @@ -225,7 +226,9 @@ impl Authorizer { /// Authenticates an HTTP request. /// - /// The method will always return authentication information. + /// The method will always return authentication information. It will also + /// return an optional token that should be added to a response as a + /// Bearer token. /// /// If there was no authentiation information in the request, the returned /// auth info will indicate an anonymous user which will fail all @@ -236,7 +239,7 @@ impl Authorizer { /// error information. pub async fn authenticate_request( &self, request: &HyperRequest - ) -> AuthInfo { + ) -> (AuthInfo, Option) { trace!("Determining actor for request {:?}", &request); // Try the legacy provider first, if any. @@ -259,13 +262,13 @@ impl Authorizer { Ok(Some(res)) => res, // authentication failure - Ok(None) => AuthInfo::anonymous(), + Ok(None) => (AuthInfo::anonymous(), None), // error during authentication - Err(err) => AuthInfo::error(err), + Err(err) => (AuthInfo::error(err), None), }; - trace!("Actor determination result: {:?}", res); + trace!("AuthInfo determination result: {:?}", res); res } @@ -372,9 +375,6 @@ pub struct AuthInfo { /// The actor for the authenticated user. actor: Actor, - /// Optional updated bearer token. - new_token: Option, - /// Access permissions. /// /// This is either a role which we consult to determine access @@ -390,7 +390,6 @@ impl AuthInfo { ) -> Self { Self { actor: Actor::user(user_id), - new_token: None, permissions: Ok(role), } } @@ -406,7 +405,6 @@ impl AuthInfo { fn anonymous() -> Self { Self { actor: Actor::anonymous(), - new_token: None, permissions: Ok(Role::anonymous().into()), } } @@ -415,28 +413,29 @@ impl AuthInfo { fn error(err: ApiAuthError) -> Self { Self { actor: Actor::anonymous(), - new_token: None, permissions: Err(err) } } - /// Sets the updated bearer token. - /// - /// If set, this new token needs to be included in an HTTP response. - pub fn set_new_token(&mut self, new_token: Token) { - self.new_token = Some(new_token); - } - - /// Takes out an updated bearer token if presnet - pub fn take_new_token(&mut self) -> Option { - self.new_token.take() - } - /// Returns a reference to the actor. pub fn actor(&self) -> &Actor { &self.actor } + /// Converts the auth info into the actor. + pub fn into_actor(self) -> Actor { + self.actor + } + + /// Returns for permissions. + pub fn has_permission( + &self, + permission: Permission, + resource: Option<&MyHandle> + ) -> bool { + self.check_permission(permission, resource).is_ok() + } + /// Checks permissions for an operation. /// /// Returns an authentication error if either the request was not diff --git a/src/server/http/auth/crypt.rs b/src/daemon/http/auth/crypt.rs similarity index 99% rename from src/server/http/auth/crypt.rs rename to src/daemon/http/auth/crypt.rs index e485d343..b31cd41a 100644 --- a/src/server/http/auth/crypt.rs +++ b/src/daemon/http/auth/crypt.rs @@ -27,7 +27,7 @@ use crate::commons::ext_serde; use crate::commons::KrillResult; use crate::commons::error::{ApiAuthError, Error}; use crate::commons::storage::{Key, Namespace, Segment}; -use crate::server::config::Config; +use crate::config::Config; const CHACHA20_KEY_BIT_LEN: usize = 256; const CHACHA20_KEY_BYTE_LEN: usize = CHACHA20_KEY_BIT_LEN / 8; diff --git a/src/server/http/auth/mod.rs b/src/daemon/http/auth/mod.rs similarity index 100% rename from src/server/http/auth/mod.rs rename to src/daemon/http/auth/mod.rs diff --git a/src/server/http/auth/permission.rs b/src/daemon/http/auth/permission.rs similarity index 100% rename from src/server/http/auth/permission.rs rename to src/daemon/http/auth/permission.rs diff --git a/src/server/http/auth/providers/admin_token.rs b/src/daemon/http/auth/providers/admin_token.rs similarity index 88% rename from src/server/http/auth/providers/admin_token.rs rename to src/daemon/http/auth/providers/admin_token.rs index 369bee0e..1ffe2512 100644 --- a/src/server/http/auth/providers/admin_token.rs +++ b/src/daemon/http/auth/providers/admin_token.rs @@ -6,10 +6,10 @@ use crate::api::admin::Token; use crate::commons::httpclient; use crate::commons::KrillResult; use crate::commons::error::{ApiAuthError, Error}; -use crate::server::config::Config; -use crate::server::http::auth::{AuthInfo, LoggedInUser, Role}; -use crate::server::http::request::HyperRequest; -use crate::server::http::response::HttpResponse; +use crate::config::Config; +use crate::daemon::http::auth::{AuthInfo, LoggedInUser, Role}; +use crate::daemon::http::request::HyperRequest; +use crate::daemon::http::response::HttpResponse; //------------ Constants ----------------------------------------------------- @@ -56,15 +56,16 @@ impl AuthProvider { /// bearer token, returns `Ok(None)`. pub fn authenticate( &self, request: &HyperRequest, - ) -> Result, ApiAuthError> { + ) -> Result)>, ApiAuthError> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); } let res = match httpclient::get_bearer_token(request) { Some(token) if token == self.required_token => { - Ok(Some(AuthInfo::user( - self.user_id.clone(), self.role.clone() + Ok(Some(( + AuthInfo::user(self.user_id.clone(), self.role.clone()), + None ))) } Some(_) => Err(ApiAuthError::ApiInvalidCredentials( @@ -105,7 +106,7 @@ impl AuthProvider { &self, request: &HyperRequest, ) -> KrillResult { - if let Ok(Some(info)) = self.authenticate(request) { + if let Ok(Some((info, _))) = self.authenticate(request) { info!("User logged out: {}", info.actor().name()); } diff --git a/src/server/http/auth/providers/config_file.rs b/src/daemon/http/auth/providers/config_file.rs similarity index 96% rename from src/server/http/auth/providers/config_file.rs rename to src/daemon/http/auth/providers/config_file.rs index 5938ffd7..8a04b44e 100644 --- a/src/server/http/auth/providers/config_file.rs +++ b/src/daemon/http/auth/providers/config_file.rs @@ -13,12 +13,12 @@ use crate::commons::httpclient; use crate::commons::KrillResult; use crate::commons::error::{ApiAuthError, Error}; use crate::constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R}; -use crate::server::config::Config; -use crate::server::http::auth::crypt; -use crate::server::http::auth::{AuthInfo, LoggedInUser, Permission, RoleMap}; -use crate::server::http::auth::session::{ClientSession, LoginSessionCache}; -use crate::server::http::request::HyperRequest; -use crate::server::http::response::HttpResponse; +use crate::config::Config; +use crate::daemon::http::auth::crypt; +use crate::daemon::http::auth::{AuthInfo, LoggedInUser, Permission, RoleMap}; +use crate::daemon::http::auth::session::{ClientSession, LoginSessionCache}; +use crate::daemon::http::request::HyperRequest; +use crate::daemon::http::response::HttpResponse; //------------ Constants ----------------------------------------------------- @@ -119,7 +119,7 @@ impl AuthProvider { pub async fn authenticate( &self, request: &HyperRequest, - ) -> Result, ApiAuthError> { + ) -> Result)>, ApiAuthError> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); } @@ -136,7 +136,7 @@ impl AuthProvider { trace!("user_id={}", session.user_id); - Ok(Some(self.auth_from_session(&session)?)) + Ok(Some((self.auth_from_session(&session)?, None))) } _ => Ok(None), }; @@ -280,7 +280,7 @@ impl AuthProvider { Some(token) => { self.session_cache.remove(&token).await; - if let Ok(Some(info)) = self.authenticate(request).await { + if let Ok(Some((info, _))) = self.authenticate(request).await { info!("User logged out: {}", info.actor().name()); } } diff --git a/src/server/http/auth/providers/mod.rs b/src/daemon/http/auth/providers/mod.rs similarity index 100% rename from src/server/http/auth/providers/mod.rs rename to src/daemon/http/auth/providers/mod.rs diff --git a/src/server/http/auth/providers/openid_connect/claims.rs b/src/daemon/http/auth/providers/openid_connect/claims.rs similarity index 100% rename from src/server/http/auth/providers/openid_connect/claims.rs rename to src/daemon/http/auth/providers/openid_connect/claims.rs diff --git a/src/server/http/auth/providers/openid_connect/config.rs b/src/daemon/http/auth/providers/openid_connect/config.rs similarity index 100% rename from src/server/http/auth/providers/openid_connect/config.rs rename to src/daemon/http/auth/providers/openid_connect/config.rs diff --git a/src/server/http/auth/providers/openid_connect/httpclient.rs b/src/daemon/http/auth/providers/openid_connect/httpclient.rs similarity index 100% rename from src/server/http/auth/providers/openid_connect/httpclient.rs rename to src/daemon/http/auth/providers/openid_connect/httpclient.rs diff --git a/src/server/http/auth/providers/openid_connect/mod.rs b/src/daemon/http/auth/providers/openid_connect/mod.rs similarity index 100% rename from src/server/http/auth/providers/openid_connect/mod.rs rename to src/daemon/http/auth/providers/openid_connect/mod.rs diff --git a/src/server/http/auth/providers/openid_connect/provider.rs b/src/daemon/http/auth/providers/openid_connect/provider.rs similarity index 99% rename from src/server/http/auth/providers/openid_connect/provider.rs rename to src/daemon/http/auth/providers/openid_connect/provider.rs index ae2fa544..b13db1a8 100644 --- a/src/server/http/auth/providers/openid_connect/provider.rs +++ b/src/daemon/http/auth/providers/openid_connect/provider.rs @@ -55,8 +55,9 @@ use serde::{Deserialize, Serialize}; use tokio::runtime; use urlparse::{urlparse, GetQuery}; -use crate::server::http::request::HyperRequest; -use crate::server::http::response::HttpResponse; +use crate::daemon::http::dispatch::AUTH_CALLBACK_ENDPOINT; +use crate::daemon::http::request::HyperRequest; +use crate::daemon::http::response::HttpResponse; use crate::{ api::admin::Token, commons::{ @@ -65,8 +66,8 @@ use crate::{ util::sha256, KrillResult, }, - server::{ - config::Config, + config::Config, + daemon::{ http::auth::{ crypt::{self, CryptState}, providers::openid_connect::{ @@ -80,7 +81,7 @@ use crate::{ session::*, AuthInfo, LoggedInUser, Permission, }, - http::server::{url_encode, AUTH_CALLBACK_ENDPOINT}, + http::util::url_encode, }, }; use super::claims::Claims; @@ -502,9 +503,7 @@ impl AuthProvider { let redirect_uri = RedirectUrl::new( self.config .service_uri() - .join( - AUTH_CALLBACK_ENDPOINT.trim_start_matches('/').as_bytes(), - ) + .join(AUTH_CALLBACK_ENDPOINT.as_bytes()) .unwrap() .to_string(), )?; @@ -1143,7 +1142,7 @@ impl AuthProvider { pub async fn authenticate( &self, request: &HyperRequest, - ) -> Result, ApiAuthError> { + ) -> Result)>, ApiAuthError> { trace!("Attempting to authenticate the request.."); self.initialize_connection_if_needed().await.map_err(|err| { @@ -1168,14 +1167,18 @@ impl AuthProvider { // return match status { SessionStatus::Active => { - return Ok(Some(self.auth_from_session(&session)?)) + return Ok(Some( + (self.auth_from_session(&session)?, None) + )) } SessionStatus::NeedsRefresh => { // If we have a refresh token try and extend the // session. Otherwise return the cached token // and continue the login session until it expires. if session.secrets.refresh_token.is_none() { - return Ok(Some(self.auth_from_session(&session)?)) + return Ok(Some( + (self.auth_from_session(&session)?, None) + )) } } SessionStatus::Expired => { @@ -1278,9 +1281,10 @@ impl AuthProvider { } }; - let mut auth = self.auth_from_session(&session)?; - auth.set_new_token(new_token); - Ok(Some(auth)) + Ok(Some(( + self.auth_from_session(&session)?, + Some(new_token) + ))) } _ => Ok(None), }; diff --git a/src/server/http/auth/providers/openid_connect/util.rs b/src/daemon/http/auth/providers/openid_connect/util.rs similarity index 100% rename from src/server/http/auth/providers/openid_connect/util.rs rename to src/daemon/http/auth/providers/openid_connect/util.rs diff --git a/src/server/http/auth/roles.rs b/src/daemon/http/auth/roles.rs similarity index 100% rename from src/server/http/auth/roles.rs rename to src/daemon/http/auth/roles.rs diff --git a/src/server/http/auth/session.rs b/src/daemon/http/auth/session.rs similarity index 100% rename from src/server/http/auth/session.rs rename to src/daemon/http/auth/session.rs diff --git a/src/daemon/http/dispatch/api.rs b/src/daemon/http/dispatch/api.rs new file mode 100644 index 00000000..66a25da3 --- /dev/null +++ b/src/daemon/http/dispatch/api.rs @@ -0,0 +1,48 @@ +//! `/api` + +use super::super::auth::Permission; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("v1") => api_v1(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + +async fn api_v1( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("authorized") => authorized(request, path), + other => { + request.check_permission(Permission::Login, None)?; + match other { + Some("bulk") => super::bulk::dispatch(request, path).await, + Some("cas") => super::cas::dispatch(request, path).await, + Some("pubd") => super::pubd::dispatch(request, path).await, + Some("ta") => super::ta::dispatch(request, path).await, + _ => Ok(HttpResponse::not_found()) + } + } + } +} + +fn authorized( + request: Request, path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + request.check_permission(Permission::Login, None).map_err(|err| { + err.with_benign(true) + })?; + Ok(HttpResponse::ok()) +} + diff --git a/src/daemon/http/dispatch/auth.rs b/src/daemon/http/dispatch/auth.rs new file mode 100644 index 00000000..2853c514 --- /dev/null +++ b/src/daemon/http/dispatch/auth.rs @@ -0,0 +1,131 @@ +//! `/auth` + +use hyper::Method; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +//------------ /auth --------------------------------------------------------- + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("login") => login(request, path).await, + Some("logout") => logout(request, path).await, + + #[cfg(feature = "multi-user")] + Some("callback") => multi_user::callback(request, path).await, + + _ => Ok(HttpResponse::not_found()) + } +} + +async fn login( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + match *request.method() { + Method::GET => { + path.check_exhausted()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(server.authorizer().get_login_url().await?) + } + Method::POST => { + path.check_exhausted()?; + let (server, request) = request.proceed_raw(); + Ok(HttpResponse::json( + &server.authorizer().login(&request).await? + )) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +async fn logout( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (server, request) = request.proceed_raw(); + Ok(server.authorizer().logout(&request).await?) +} + + +#[cfg(feature = "multi-user")] +mod multi_user { + use base64::engine::Engine as _; + use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; + use log::trace; + use crate::commons::error::Error; + use crate::daemon::http::auth::LoggedInUser; + use crate::daemon::http::util::url_encode; + use super::*; + + pub async fn callback( + request: Request<'_>, + path: PathIter<'_>, + ) -> Result { + path.check_exhausted()?; + request.check_get()?; + + trace!( + "Authentication callback invoked: {:?}", &request.hyper() + ); + + let (server, request) = request.proceed_raw(); + server.authorizer().login(&request).await.and_then(|user| { + build_auth_redirect_location(user).map_err(|err| { + Error::custom(format!( + "Unable to build redirect with logged in user details: \ + {:?}", + err + )) + }) + }).map(|location| { + HttpResponse::found(&location) + }).or_else(|err| { + // 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. + let location = match serde_json::to_string( + &err.to_error_response() + ) { + Ok(json) => { + format!("/ui/login?error={}", BASE64_ENGINE.encode(json)) + } + Err(_) => String::from("/ui/login") + }; + Ok(HttpResponse::found(&location)) + }) + } + + fn build_auth_redirect_location( + user: LoggedInUser + ) -> Result { + fn b64_encode_attributes_with_mapped_error( + a: &impl serde::Serialize, + ) -> Result { + Ok(BASE64_ENGINE.encode( + serde_json::to_string(a) + .map_err(|err| Error::custom(err.to_string()))?, + )) + } + + let attributes = b64_encode_attributes_with_mapped_error( + user.attributes() + )?; + + Ok(format!( + "/ui/login?token={}&id={}&attributes={}", + &url_encode(user.token())?, + &url_encode(user.id())?, + &url_encode(attributes)?, + )) + } +} + diff --git a/src/daemon/http/dispatch/bulk.rs b/src/daemon/http/dispatch/bulk.rs new file mode 100644 index 00000000..539ae62c --- /dev/null +++ b/src/daemon/http/dispatch/bulk.rs @@ -0,0 +1,139 @@ +//! `/api/v1/bulk` + +use crate::api::ca::AllCertAuthIssues; +use super::super::auth::Permission; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("cas") => cas(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + +async fn cas( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("import") => cas_import(request, path).await, + Some("issues") => cas_issues(request, path), + Some("sync") => cas_sync(request, path), + Some("publish") => cas_publish(request, path), + Some("force_publish") => cas_force_publish(request, path), + Some("suspend") => cas_suspend(request, path), + _ => Ok(HttpResponse::not_found()) + } +} + +async fn cas_import( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let (server, structure) = request.read_json().await?; + server.krill().cas_import(structure).await?; + Ok(HttpResponse::ok()) +} + +fn cas_issues( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, auth) = request.proceed_unchecked(); + let server = request.empty()?; + + let mut all_issues = AllCertAuthIssues::default(); + for ca in server.krill().ca_handles()? { + if auth.has_permission(Permission::CaRead, Some(&ca)) { + let issues = server.krill().ca_issues(&ca)?; + if !issues.is_empty() { + all_issues.cas.insert(ca, issues); + } + } + } + + Ok(HttpResponse::json(&all_issues)) +} + +fn cas_sync( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("parent") => cas_sync_parent(request, path), + Some("repo") => cas_sync_repo(request, path), + _ => Ok(HttpResponse::not_found()) + } +} + +fn cas_sync_parent( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + server.krill().cas_refresh_all()?; + Ok(HttpResponse::ok()) +} + +fn cas_sync_repo( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + server.krill().cas_repo_sync_all()?; + Ok(HttpResponse::ok()) +} + +fn cas_publish( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + server.krill().republish_all(false)?; + Ok(HttpResponse::ok()) +} + +fn cas_force_publish( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + server.krill().republish_all(true)?; + Ok(HttpResponse::ok()) +} + +fn cas_suspend( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + server.krill().cas_schedule_suspend_all()?; + Ok(HttpResponse::ok()) +} + diff --git a/src/daemon/http/dispatch/cas.rs b/src/daemon/http/dispatch/cas.rs new file mode 100644 index 00000000..6ff5ed15 --- /dev/null +++ b/src/daemon/http/dispatch/cas.rs @@ -0,0 +1,1085 @@ +//! `/api/v1/cas` + +use bytes::Bytes; +use hyper::Method; +use rpki::ca::idexchange::{ + CaHandle, ChildHandle, ParentHandle, ParentResponse, + RepositoryResponse, +}; +use crate::api::admin::{ApiRepositoryContact, ParentCaReq, RepositoryContact}; +use crate::api::aspa::AspaDefinitionUpdates; +use crate::api::bgp::BgpAnalysisAdvice; +use crate::api::ca::{CertAuthList, CertAuthSummary}; +use crate::api::import::ImportChild; +use crate::api::history::CommandHistoryCriteria; +use crate::api::roa::RoaConfigurationUpdates; +use crate::commons::error::Error; +use crate::commons::eventsourcing::AggregateStoreError; +use super::super::auth::Permission; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +//------------ /api/v1/cas --------------------------------------------------- + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.parse_opt_next()? { + None => index(request).await, + Some(handle) => ca(request, path, handle).await, + } +} + +async fn index( + request: Request<'_>, +) -> Result { + match *request.method() { + Method::GET => index_get(request), + Method::POST => index_post(request).await, + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn index_get( + request: Request<'_>, +) -> Result { + let (request, auth) = request.proceed_unchecked(); + let server = request.empty()?; + + Ok(HttpResponse::json( + &CertAuthList { + cas: { + server.krill().ca_handles()?.filter_map(|handle| { + auth.has_permission( + Permission::CaRead, Some(&handle) + ).then_some(CertAuthSummary { handle }) + }).collect() + } + } + )) +} + +async fn index_post( + request: Request<'_>, +) -> Result { + let (request, _) = request.proceed_permitted( + Permission::CaCreate, None + )?; + let (server, init) = request.read_json().await?; + server.krill().ca_init(init)?; + Ok(HttpResponse::ok()) +} + + +//------------ /api/v1/cas/{ca} ---------------------------------------------- + +pub async fn ca( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + request.check_permission(Permission::CaRead, Some(&ca))?; + + match path.next() { + None => ca_index(request, ca).await, + Some("aspas") => aspas(request, path, ca).await, + Some("bgpsec") => bgpsec(request, path, ca).await, + Some("children") => children(request, path, ca).await, + Some("history") => history(request, path, ca), + Some("id") => id(request, path, ca), + Some("issues") => issues(request, path, ca), + Some("keys") => keys(request, path, ca), + Some("parents") => parents(request, path, ca).await, + Some("repo") => repo(request, path, ca).await, + Some("routes") => routes(request, path, ca).await, + Some("stats") => stats(request, path, ca), + Some("sync") => sync(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +async fn ca_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_info(&ca)? + )) + } + Method::DELETE => { + let (request, auth) = request.proceed_permitted( + Permission::CaDelete, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_delete(&ca, auth.actor()).await?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + + +//------------ /api/v1/cas/{ca}/aspas ---------------------------------------- + +async fn aspas( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + None => aspas_index(request, ca).await, + Some("as") => aspas_as(request, path, ca).await, + _ => Ok(HttpResponse::not_found()) + } +} + +async fn aspas_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::AspasRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_aspas_definitions_show(&ca)? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::AspasUpdate, Some(&ca) + )?; + let (server, updates) = request.read_json().await?; + server.krill().ca_aspas_definitions_update( + ca, updates, auth.actor(), + )?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +async fn aspas_as( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + let customer = path.parse_next()?; + path.check_exhausted()?; + match *request.method() { + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::AspasUpdate, Some(&ca) + )?; + let (server, update) = request.read_json().await?; + server.krill().ca_aspas_update_aspa( + ca, customer, update, auth.actor() + )?; + Ok(HttpResponse::ok()) + } + Method::DELETE => { + let (request, auth) = request.proceed_permitted( + Permission::AspasUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_aspas_definitions_update( + ca, + AspaDefinitionUpdates { + add_or_replace: Vec::new(), + remove: vec![customer] + }, + auth.actor(), + )?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + + +//------------ /api/v1/cas/{ca}/bgpsec --------------------------------------- + +async fn bgpsec( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::BgpsecRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_bgpsec_definitions_show(&ca)? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::BgpsecUpdate, Some(&ca) + )?; + let (server, updates) = request.read_json().await?; + server.krill().ca_bgpsec_definitions_update( + ca, updates, auth.actor() + )?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + + +//------------ /api/v1/cas/{ca}/children ------------------------------------- + +async fn children( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.parse_opt_next()? { + None => children_index(request, ca).await, + Some(child) => children_child(request, path, ca, child).await, + } +} + +async fn children_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let (server, child_req) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().ca_add_child(&ca, child_req, auth.actor())? + )) +} + +async fn children_child( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, + child: ChildHandle, +) -> Result { + match path.next() { + None => children_child_index(request, ca, child).await, + Some("contact") | Some("parent_response.json") => { + children_child_contact(request, path, ca, child) + } + Some("parent_response.xml") => { + children_child_contact_xml(request, path, ca, child) + } + Some("export") => children_child_export(request, path, ca, child), + Some("import") => { + children_child_import(request, path, ca, child).await + } + _ => Ok(HttpResponse::not_found()) + } +} + +async fn children_child_index( + request: Request<'_>, + ca: CaHandle, + child: ChildHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_child_show(&ca, &child)? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let (server, child_req) = request.read_json().await?; + server.krill().ca_child_update( + &ca, child, child_req, auth.actor() + )?; + Ok(HttpResponse::ok()) + } + Method::DELETE => { + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_child_remove(&ca, child, auth.actor())?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn children_child_contact( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_parent_response(&ca, child)? + )) +} + +fn children_child_contact_xml( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + let res = server.krill().ca_parent_response(&ca, child)?; + Ok(HttpResponse::xml(res.to_xml_vec())) +} + +fn children_child_export( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_child_export(&ca, &child)? + )) +} + +async fn children_child_import( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, Some(&ca) + )?; + let (server, import) = request.read_json::().await?; + if import.name != child { + return Ok(HttpResponse::response_from_error( + Error::CaChildImportHandleMismatch { + path: child, body: import.name + } + )) + } + server.krill().ca_child_import(&ca, import, auth.actor())?; + Ok(HttpResponse::ok()) +} + + +//------------ /api/v1/cas/{ca}/history -------------------------------------- + +fn history( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + Some("commands") => history_commands(request, path, ca), + Some("details") => history_details(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +fn history_commands( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + let mut path = path.strip_trailing_slash(); + let rows_limit = Some(path.parse_opt_next()?.unwrap_or(100)); + let offset = path.parse_opt_next()?.unwrap_or(0); + let after = path.parse_opt_next()?; + let before = path.parse_opt_next()?; + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + + Ok(HttpResponse::json( + &server.krill().ca_history( + &ca, + CommandHistoryCriteria { + before, after, offset, rows_limit, + .. Default::default() + } + )? + )) +} + +fn history_details( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + let version = path.parse_next()?; + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + + Ok(HttpResponse::json( + &server.krill().ca_command_details(&ca, version).map_err(|err| { + match err { + Error::AggregateStoreError( + AggregateStoreError::UnknownCommand(..) + ) => { + HttpResponse::not_found() + }, + err => { + HttpResponse::response_from_error(err) + } + } + })? + )) +} + + +//------------ /api/v1/cas/{ca}/id ------------------------------------------- + +fn id( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + None => id_index(request, ca), + Some("child_request.json") => { + id_child_request_json(request, path, ca) + } + Some("child_request.xml") => id_child_request_xml(request, path, ca), + Some("publisher_request.json") => { + id_publisher_request_json(request, path, ca) + } + Some("publisher_request.xml") => { + id_publisher_request_xml(request, path, ca) + } + _ => Ok(HttpResponse::not_found()) + } +} + +fn id_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_update_id(ca, auth.actor())?; + Ok(HttpResponse::ok()) +} + +fn id_child_request_json( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_child_req(&ca)? + )) +} + +fn id_child_request_xml( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().ca_child_req(&ca)?.to_xml_vec() + )) +} + +fn id_publisher_request_json( + request: Request, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_publisher_req(&ca)? + )) +} + +fn id_publisher_request_xml( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().ca_publisher_req(&ca)?.to_xml_vec() + )) +} + + +//------------ /api/v1/cas/{ca}/issues --------------------------------------- + +fn issues( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_issues(&ca)? + )) +} + + +//------------ /api/v1/cas/{ca}/keys ----------------------------------------- + +fn keys( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + Some("roll_init") => keys_roll_init(request, path, ca), + Some("roll_activate") => keys_roll_activate(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +fn keys_roll_init( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_keyroll_init(ca, auth.actor())?; + Ok(HttpResponse::ok()) +} + +fn keys_roll_activate( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_keyroll_activate(ca, auth.actor())?; + Ok(HttpResponse::ok()) +} + + +//------------ /api/v1/cas/{ca}/parents -------------------------------------- + +async fn parents( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.parse_opt_next()? { + None => parents_index(request, ca).await, + Some(parent) => parents_parent(request, path, ca, parent).await, + } +} + +async fn parents_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_status(&ca)?.into_parents() + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let (server, bytes) = request.read_bytes().await?; + let parent_req = extract_parent_ca_req(&ca, bytes, None)?; + server.krill().ca_parent_add_or_update( + ca, parent_req, auth.actor() + ).await?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +async fn parents_parent( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, + parent: ParentHandle, +) -> Result { + path.check_exhausted()?; + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_my_parent_contact(&ca, &parent)? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let (server, bytes) = request.read_bytes().await?; + let parent_req = extract_parent_ca_req( + &ca, bytes, Some(parent) + )?; + server.krill().ca_parent_add_or_update( + ca, parent_req, auth.actor() + ).await?; + Ok(HttpResponse::ok()) + } + Method::DELETE => { + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().ca_parent_remove(ca, parent, auth.actor()).await?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn extract_parent_ca_req( + ca: &CaHandle, + bytes: Bytes, + parent_override: Option, +) -> Result { + // We distinguis between XML and JSON by looking at the first + // non-whitespace character which should be '<' for XML. + let bytes = bytes.trim_ascii_start(); + if bytes.first().copied() == Some(b'<') { + let response = ParentResponse::parse(bytes).map_err(|err| { + Error::CaParentResponseInvalid( + ca.clone(), + err.to_string(), + ) + })?; + + let parent_name = parent_override.unwrap_or_else(|| { + response.parent_handle().clone() + }); + + Ok(ParentCaReq { handle: parent_name, response }) + } + else { + let req: ParentCaReq = serde_json::from_slice(bytes).map_err( + Error::JsonError + )?; + if let Some(parent_override) = parent_override { + if req.handle != parent_override { + return Err(Error::Custom(format!( + "Used different parent names on path ({}) and \ + submitted JSON ({}) for adding/updating a parent", + parent_override, + req.handle + ))); + } + } + Ok(req) + } +} + + +//------------ /api/v1/cas/{ca}/repo ----------------------------------------- + +async fn repo( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + None => repo_index(request, ca).await, + Some("status") => repo_status(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +async fn repo_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_repo_details(&ca)? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let (server, update) = request.read_bytes().await?; + let update = extract_repository_contact(&ca, update)?; + server.krill().ca_repo_update(ca, update, auth.actor()).await?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +pub fn extract_repository_contact( + ca: &CaHandle, + bytes: Bytes, +) -> Result { + // We distinguis between XML and JSON by looking at the first + // non-whitespace character which should be '<' for XML. + let bytes = bytes.trim_ascii_start(); + if bytes.first().copied() == Some(b'<') { + let response = RepositoryResponse::parse(bytes).map_err(|err| { + Error::CaRepoResponseInvalid( + ca.clone(), + err.to_string(), + ) + })?; + RepositoryContact::try_from_response(response).map_err(|err| { + Error::CaRepoResponseInvalid(ca.clone(), err.to_string()) + }) + } + else { + let api_contact: ApiRepositoryContact = serde_json::from_slice( + bytes + ).map_err(Error::JsonError)?; + RepositoryContact::try_from_response(api_contact.repository_response) + } +} + +fn repo_status( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_status(&ca)?.into_repo() + )) +} + + +//------------ /api/v1/cas/{ca}/routes --------------------------------------- + +async fn routes( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + None => routes_index(request, ca).await, + Some("try") => routes_try(request, path, ca).await, + Some("analysis") => routes_analysis(request, path, ca).await, + _ => Ok(HttpResponse::not_found()) + } +} + +async fn routes_index( + request: Request<'_>, + ca: CaHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::RoutesRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_routes_show(&ca)? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::RoutesUpdate, Some(&ca) + )?; + let (server, updates) = request.read_json().await?; + server.krill().ca_routes_update(ca, updates, auth.actor())?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +async fn routes_try( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::RoutesUpdate, Some(&ca) + )?; + let (server, mut updates) + = request.read_json::().await?; + let effect = server.krill().ca_routes_bgp_dry_run( + &ca, updates.clone() + ).await?; + if effect.contains_invalids() { + updates.set_explicit_max_length(); + let resources = updates.affected_prefixes(); + let suggestion = server.krill().ca_routes_bgp_suggest( + &ca, Some(resources) + ).await?; + Ok(HttpResponse::json( + &BgpAnalysisAdvice { + effect, suggestion, + } + )) + } + else { + server.krill().ca_routes_update(ca, updates, auth.actor())?; + Ok(HttpResponse::ok()) + } +} + +async fn routes_analysis( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + Some("full") => routes_analysis_full(request, path, ca).await, + Some("dryrun") => routes_analysis_dryrun(request, path, ca).await, + Some("suggest") => routes_analysis_suggest(request, path, ca).await, + _ => Ok(HttpResponse::not_found()) + } +} + +async fn routes_analysis_full( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::RoutesAnalysis, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_routes_bgp_analysis(&ca).await? + )) +} + +async fn routes_analysis_dryrun( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted( + Permission::RoutesAnalysis, Some(&ca) + )?; + let (server, updates) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().ca_routes_bgp_dry_run(&ca, updates).await? + )) +} + +async fn routes_analysis_suggest( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::RoutesAnalysis, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_routes_bgp_suggest(&ca, None).await? + )) + } + Method::POST => { + let (request, _) = request.proceed_permitted( + Permission::RoutesAnalysis, Some(&ca) + )?; + let (server, resources) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().ca_routes_bgp_suggest( + &ca, Some(resources) + ).await? + )) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + + +//------------ /api/v1/cas/{ca}/stats ---------------------------------------- + +fn stats( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + Some("children") => stats_children(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +fn stats_children( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + Some("connections") => stats_children_connections(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +fn stats_children_connections( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaRead, Some(&ca) + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_stats_child_connections(&ca)? + )) +} + + +//------------ /api/v1/cas/{ca}/sync ----------------------------------------- + +fn sync( + request: Request<'_>, + mut path: PathIter<'_>, + ca: CaHandle, +) -> Result { + match path.next() { + Some("parents") => sync_parents(request, path, ca), + Some("repo") => sync_repo(request, path, ca), + _ => Ok(HttpResponse::not_found()) + } +} + +fn sync_parents( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().cas_refresh_single(ca)?; + Ok(HttpResponse::ok()) +} + +fn sync_repo( + request: Request<'_>, + path: PathIter<'_>, + ca: CaHandle, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted( + Permission::CaUpdate, Some(&ca) + )?; + let server = request.empty()?; + server.krill().cas_repo_sync_single(&ca)?; + Ok(HttpResponse::ok()) +} + diff --git a/src/daemon/http/dispatch/error.rs b/src/daemon/http/dispatch/error.rs new file mode 100644 index 00000000..47f5e028 --- /dev/null +++ b/src/daemon/http/dispatch/error.rs @@ -0,0 +1,38 @@ +//! Dispatch error handling. + +use crate::commons::error::{Error, FatalError}; +use super::super::response::HttpResponse; + + +//------------ DispatchError ------------------------------------------------- + +/// An error occured during dispatch. +/// +/// This error type exists so you can use the question mark operator for all +/// sorts of things during dispatch to minimize clutter. +/// +/// The error can either be a response sent back to the client or a fatal +/// error ending the server. Various `From<_>` impls are provided to correctly +/// translate errors into one of the two cases. +#[derive(Debug)] +pub enum DispatchError { + /// A response should be sent to the client. + Response(HttpResponse), + + /// A fatal error happened that should terminate the server. + #[allow(dead_code)] + Fatal(FatalError), +} + +impl From for DispatchError { + fn from(src: HttpResponse) -> Self { + Self::Response(src) + } +} + +impl From for DispatchError { + fn from(src: Error) -> Self { + Self::Response(HttpResponse::response_from_error(src)) + } +} + diff --git a/src/server/http/metrics.rs b/src/daemon/http/dispatch/metrics.rs similarity index 95% rename from src/server/http/metrics.rs rename to src/daemon/http/dispatch/metrics.rs index 5d480f3b..bed8d2c2 100644 --- a/src/server/http/metrics.rs +++ b/src/daemon/http/dispatch/metrics.rs @@ -4,16 +4,20 @@ use std::fmt; use std::collections::HashMap; use std::fmt::Write; use crate::constants::TA_NAME; -use super::request::Request; -use super::response::HttpResponse; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; -pub async fn metrics(req: Request) -> Result { - if !req.is_get() || !req.path().segment().starts_with("metrics") { - return Err(req) - } +pub async fn dispatch( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; - let server = req.state(); let mut target = Target::default(); target.single( @@ -52,21 +56,21 @@ pub async fn metrics(req: Request) -> Result { "auth_session_cache_size", "total number of cached login session tokens", ), - server.login_session_cache_size().await, + server.authorizer().login_session_cache_size().await, ); - if let Ok(cas_stats) = server.cas_stats().await { + if let Ok(cas_stats) = server.krill().cas_stats().await { target.single( Metric::gauge("cas", "number of CAs in Krill"), cas_stats.len() ); - if !server.config.metrics.metrics_hide_ca_details { + if !server.config().metrics.metrics_hide_ca_details { let mut ca_status_map = HashMap::new(); for ca in cas_stats.keys() { - if let Ok(ca_status) = server.ca_status(ca) { + if let Ok(ca_status) = server.krill().ca_status(ca) { ca_status_map.insert(ca.clone(), ca_status); } } @@ -155,7 +159,7 @@ pub async fn metrics(req: Request) -> Result { }); if any_children - && !server.config.metrics.metrics_hide_child_details + && !server.config().metrics.metrics_hide_child_details { let metric = Metric::gauge( "cas_children", @@ -279,7 +283,7 @@ pub async fn metrics(req: Request) -> Result { } } - if !server.config.metrics.metrics_hide_roa_details { + if !server.config().metrics.metrics_hide_roa_details { let metric = Metric::gauge( "cas_bgp_announcements_valid", "number of announcements seen for CA resources \ @@ -382,7 +386,7 @@ pub async fn metrics(req: Request) -> Result { } } - if let Ok(stats) = server.repo_stats() { + if let Ok(stats) = server.krill().repo_stats() { target.single( Metric::gauge( "repo_publisher", @@ -409,7 +413,7 @@ pub async fn metrics(req: Request) -> Result { stats.serial ); - if !server.config.metrics.metrics_hide_publisher_details { + if !server.config().metrics.metrics_hide_publisher_details { let metric = Metric::gauge( "repo_objects", "number of objects in repository for publisher" diff --git a/src/daemon/http/dispatch/mod.rs b/src/daemon/http/dispatch/mod.rs new file mode 100644 index 00000000..384a905a --- /dev/null +++ b/src/daemon/http/dispatch/mod.rs @@ -0,0 +1,25 @@ +//! Dispatching of HTTP requests. + +pub use self::error::DispatchError; +pub use self::root::dispatch_request; + +/// The authentication callback path used by the OpenID provider. +/// +/// This must not start with a slash. +/// +/// It must also resolve to be dispatched to `self::auth::callback`. +#[cfg(feature = "multi-user")] +pub const AUTH_CALLBACK_ENDPOINT: &str = "auth/callback"; + +mod api; +mod auth; +mod cas; +mod bulk; +mod error; +mod metrics; +mod pubd; +mod root; +mod stats; +mod ta; +mod testbed; + diff --git a/src/daemon/http/dispatch/pubd.rs b/src/daemon/http/dispatch/pubd.rs new file mode 100644 index 00000000..cc58385d --- /dev/null +++ b/src/daemon/http/dispatch/pubd.rs @@ -0,0 +1,231 @@ +//! `/api/v1/pubd` + +use hyper::Method; +use rpki::ca::idexchange::PublisherHandle; +use crate::api::admin::{PublisherList, PublisherSummary}; +use super::super::auth::Permission; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + +//------------ /api/v1/pubd -------------------------------------------------- + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + request.check_permission(Permission::PubAdmin, None)?; + + match path.next() { + Some("delete") => delete(request, path).await, + Some("init") => init(request, path).await, + Some("publishers") => publishers(request, path).await, + Some("session_reset") => session_reset(request, path), + Some("stale") => stale(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + + +//------------ /api/v1/pubd/delete ------------------------------------------- + +async fn delete( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted( + Permission::PubAdmin, None + )?; + let (server, criteria) = request.read_json().await?; + server.krill().delete_matching_files(criteria)?; + Ok(HttpResponse::ok()) +} + + +//------------ /api/v1/pubd/init --------------------------------------------- + +async fn init( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + match *request.method() { + Method::POST => { + let (request, _) = request.proceed_permitted( + Permission::PubAdmin, None + )?; + let (server, uris) = request.read_json().await?; + server.krill().repository_init(uris)?; + Ok(HttpResponse::ok()) + } + Method::DELETE => { + let (request, _) = request.proceed_permitted( + Permission::PubAdmin, None + )?; + let server = request.empty()?; + server.krill().repository_clear()?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + + +//------------ /api/v1/pubd/publishers --------------------------------------- + +async fn publishers( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.parse_opt_next()? { + None => publishers_index(request).await, + Some(publisher) => { + publishers_publisher(request, path, publisher) + } + } +} + +async fn publishers_index( + request: Request<'_>, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::PubList, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &PublisherList { + publishers: { + server.krill().publishers()?.into_iter().map( + PublisherSummary::from_handle + ).collect() + } + } + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::PubCreate, None + )?; + let (server, pbl) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().add_publisher(pbl, auth.actor())? + )) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn publishers_publisher( + request: Request<'_>, + mut path: PathIter<'_>, + publisher: PublisherHandle, +) -> Result { + match path.next() { + None => publishers_publisher_index(request, publisher), + Some("response.json") => { + publishers_publisher_response(request, path, publisher) + } + Some("response.xml") => { + publishers_publisher_response_xml(request, path, publisher) + } + _ => Ok(HttpResponse::not_found()) + } +} + +fn publishers_publisher_index( + request: Request<'_>, + publisher: PublisherHandle, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::PubRead, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().get_publisher(publisher)? + )) + } + Method::DELETE => { + let (request, auth) = request.proceed_permitted( + Permission::PubDelete, None + )?; + let server = request.empty()?; + server.krill().remove_publisher(publisher, auth.actor())?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn publishers_publisher_response( + request: Request<'_>, + path: PathIter<'_>, + publisher: PublisherHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted(Permission::PubRead, None)?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().repository_response(&publisher)? + )) +} + +fn publishers_publisher_response_xml( + request: Request<'_>, + path: PathIter<'_>, + publisher: PublisherHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted(Permission::PubRead, None)?; + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().repository_response(&publisher)?.to_xml_vec() + )) +} + + +//------------ /api/v1/pubd/session_reset ------------------------------------ + +fn session_reset( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted(Permission::PubAdmin, None)?; + let server = request.empty()?; + server.krill().repository_session_reset()?; + Ok(HttpResponse::ok()) +} + + +//------------ /api/v1/pubd/stale -------------------------------------------- + +async fn stale( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + let seconds = path.parse_opt_next()?.unwrap_or(0); + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( Permission::PubList, None)?; + let server = request.empty()?; + let stats = server.krill().repo_stats()?; + Ok(HttpResponse::json( + &PublisherList { + publishers: { + stats.stale_publishers(seconds).map( + PublisherSummary::from_handle + ).collect() + } + } + )) +} + diff --git a/src/daemon/http/dispatch/root.rs b/src/daemon/http/dispatch/root.rs new file mode 100644 index 00000000..1a0294a7 --- /dev/null +++ b/src/daemon/http/dispatch/root.rs @@ -0,0 +1,221 @@ +//! Root level. + +use std::fs; +use super::super::request::{Request, PathIter}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +//------------ / ------------------------------------------------------------- + +pub async fn dispatch_request( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + None => index(request), + Some("api") => super::api::dispatch(request, path).await, + Some("assets") => assets(request, path), + Some("auth") => super::auth::dispatch(request, path).await, + Some("health") => health(request, path), + Some("metrics") => super::metrics::dispatch(request, path).await, + Some("rfc8181") => rfc8181(request, path).await, + Some("rfc6492") => rfc6492(request, path).await, + Some("rrdp") => rrdp(request, path), + Some("stats") => super::stats::dispatch(request, path).await, + Some("ta") => ta(request, path), + Some("testbed.tal") => tal(request, path), + Some("testbed") => super::testbed::dispatch(request, path).await, + Some("ui") => ui(request, path), + + // statics + // + _ => Ok(HttpResponse::not_found()) + } +} + + +//------------ / ------------------------------------------------------------- + +fn index( + request: Request<'_> +) -> Result { + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + request.empty()?; + Ok(HttpResponse::found("/ui")) +} + + +//------------ /health ------------------------------------------------------- + +fn health( + request: Request<'_>, path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + request.empty()?; + Ok(HttpResponse::ok()) +} + + +//------------ /rfc8181 ------------------------------------------------------ + +async fn rfc8181( + request: Request<'_>, path: PathIter<'_> +) -> Result { + // We need to allow trailing slashes for compatibility. + let mut path = path.strip_trailing_slash(); + + let publisher = path.parse_next()?; + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_unchecked(); + let (server, bytes) = request.read_rfc8181_bytes().await?; + Ok(HttpResponse::rfc8181( + server.krill().rfc8181(publisher, bytes)? + )) +} + + +//------------ /rfc6492 ------------------------------------------------------ + +async fn rfc6492( + request: Request<'_>, path: PathIter<'_> +) -> Result { + // We need to allow trailing slashes for compatibility. + let mut path = path.strip_trailing_slash(); + + let ca = path.parse_next()?; + path.check_exhausted()?; + request.check_post()?; + let user_agent = request.user_agent(); + let (request, auth) = request.proceed_unchecked(); + let (server, bytes) = request.read_rfc6492_bytes().await?; + // XXX Using auth.actor() here doesn’t make much sense -- it likely will + // always be the anonymous actor. Maybe the CA manager should + // determine the actor when looking at the ID certificate? + Ok(HttpResponse::rfc6492( + server.krill().rfc6492(ca , bytes, user_agent, auth.actor())? + )) +} + + +//------------ /ta ----------------------------------------------------------- + +fn ta( + request: Request<'_>, mut path: PathIter<'_> +) -> Result { + match path.next() { + Some("ta.tal") => tal(request, path), + Some("ta.cer") => ta_cer(request, path), + _ => Ok(HttpResponse::not_found()) + } +} + +fn tal( + request: Request<'_>, path: PathIter<'_> +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::text( + server.krill().ta_cert_details()?.tal.to_string() + )) +} + +fn ta_cer( + request: Request<'_>, path: PathIter<'_> +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::cert( + server.krill().ta_cert_details()?.cert.to_bytes() + )) +} + + +//------------ /rrdp --------------------------------------------------------- + +fn rrdp( + request: Request<'_>, path: PathIter<'_> +) -> Result { + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + let Some(remaining) = path.remaining() else { + return Ok(HttpResponse::not_found()) + }; + let path = match server.krill().resolve_rrdp_request_path(remaining)? { + Some(path) => path, + None => { + return Ok(HttpResponse::not_found()) + } + }; + + let cache_seconds = if remaining == "notification.xml" { + 60 + } else { + 86400 + }; + + let buffer = match fs::read(&path) { + Ok(file) => file, + Err(_) => { + return Ok(HttpResponse::not_found()) + } + }; + Ok(HttpResponse::xml_with_cache(buffer, cache_seconds)) +} + + +//------------ /ui ----------------------------------------------------------- + +fn ui( + request: Request<'_>, path: PathIter<'_> +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let _server = request.empty()?; + Ok(HttpResponse::ok_with_body( + assets::INDEX.media_type, + assets::INDEX.content, + )) +} + + +//------------ /assets ------------------------------------------------------- + +fn assets( + request: Request<'_>, mut path: PathIter<'_> +) -> Result { + let asset = { + let Some(next) = path.next() else { + return Ok(HttpResponse::not_found()) + }; + match assets::ASSETS.iter().find(|asset| { + asset.path == next + }) { + Some(asset) => asset, + None => return Ok(HttpResponse::not_found()), + } + }; + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let _server = request.empty()?; + Ok(HttpResponse::ok_with_body( + asset.media_type, + asset.content, + )) +} + +mod assets { + include!(concat!(env!("OUT_DIR"), "/ui_assets.rs")); +} + diff --git a/src/daemon/http/dispatch/stats.rs b/src/daemon/http/dispatch/stats.rs new file mode 100644 index 00000000..291e63d6 --- /dev/null +++ b/src/daemon/http/dispatch/stats.rs @@ -0,0 +1,63 @@ +//! `/api/v1/ta` + +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +//------------ /stats -------------------------------------------------------- + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("info") => info(request, path), + Some("repo") => repo(request, path), + Some("cas") => cas(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + + +//------------ /stats/info --------------------------------------------------- + +fn info( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::json(&server.server_info())) +} + + +//------------ /stats/repo --------------------------------------------------- + +fn repo( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::json(&server.krill().repo_stats()?)) +} + + +//------------ /stats/cas ---------------------------------------------------- + +async fn cas( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::json(&server.krill().cas_stats().await?)) +} + diff --git a/src/daemon/http/dispatch/ta.rs b/src/daemon/http/dispatch/ta.rs new file mode 100644 index 00000000..7755b4db --- /dev/null +++ b/src/daemon/http/dispatch/ta.rs @@ -0,0 +1,340 @@ +//! `/api/v1/ta` + +use hyper::Method; +use rpki::ca::idexchange::ChildHandle; +use crate::commons::error::Error; +use crate::constants::ta_handle; +use super::super::auth::Permission; +use super::super::request::{PathIter, Request}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +//------------ /api/v1/ta ---------------------------------------------------- + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("proxy") => proxy(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + + +//------------ /api/v1/proxy ------------------------------------------------- + +async fn proxy( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("children") => proxy_children(request, path).await, + Some("id") => proxy_id(request, path), + Some("init") => proxy_init(request, path), + Some("repo") => proxy_repo(request, path).await, + Some("signer") => proxy_signer(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + + +//------------ /api/v1/proxy/children ---------------------------------------- + +async fn proxy_children( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.parse_opt_next()? { + None => proxy_children_index(request).await, + Some(child) => proxy_children_child(request, path, child), + } +} + +async fn proxy_children_index( + request: Request<'_>, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + request.empty()?; + Err(Error::NotImplemented.into()) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let (server, child) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().ta_proxy_children_add(child, auth.actor())? + )) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn proxy_children_child( + request: Request<'_>, + mut path: PathIter<'_>, + child: ChildHandle, +) -> Result { + match path.next() { + None => proxy_children_child_index(request, child), + Some("parent_response.json") => { + proxy_children_child_response(request, path, child) + } + Some("parent_response.xml") => { + proxy_children_child_response_xml(request, path, child) + } + _ => Ok(HttpResponse::not_found()) + } +} + +fn proxy_children_child_index( + request: Request<'_>, + _child: ChildHandle, +) -> Result { + match *request.method() { + Method::POST => { + request.proceed_permitted(Permission::CaAdmin, None)?; + // Ignore body for now. + Err(Error::NotImplemented.into()) + } + Method::DELETE => { + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + request.empty()?; + Err(Error::NotImplemented.into()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn proxy_children_child_response( + request: Request<'_>, + path: PathIter<'_>, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ca_parent_response(&ta_handle(), child)? + )) +} + +fn proxy_children_child_response_xml( + request: Request<'_>, + path: PathIter<'_>, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().ca_parent_response(&ta_handle(), child)?.to_xml_vec() + )) +} + + +//------------ /api/v1/proxy/init -------------------------------------------- + +fn proxy_init( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + server.krill().ta_proxy_init()?; + Ok(HttpResponse::ok()) +} + + +//------------ /api/v1/proxy/id ---------------------------------------------- + +fn proxy_id( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ta_proxy_id()? + )) +} + + +//------------ /api/v1/proxy/repo -------------------------------------------- + +async fn proxy_repo( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + None => proxy_repo_index(request).await, + Some("request.json") => proxy_repo_request(request, path), + Some("request.xml") => proxy_repo_request_xml(request, path), + _ => Ok(HttpResponse::not_found()) + } +} + +async fn proxy_repo_index( + request: Request<'_>, +) -> Result { + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ta_proxy_repository_contact()? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let (server, update) = request.read_bytes().await?; + let update = super::cas::extract_repository_contact( + &ta_handle(), update + )?; + server.krill().ta_proxy_repository_update(update, auth.actor())?; + Ok(HttpResponse::ok()) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +fn proxy_repo_request( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ta_proxy_publisher_request()? + )) +} + +fn proxy_repo_request_xml( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_permitted(Permission::CaAdmin, None)?; + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().ta_proxy_publisher_request()?.to_xml_vec() + )) +} + + +//------------ /api/v1/proxy/signer ------------------------------------------ + +async fn proxy_signer( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.next() { + Some("add") => proxy_signer_add(request, path).await, + Some("request") => proxy_signer_request(request, path), + Some("response") => proxy_signer_response(request, path).await, + Some("update") => proxy_signer_update(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + +async fn proxy_signer_add( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let (server, info) = request.read_json().await?; + server.krill().ta_proxy_signer_add(info, auth.actor())?; + Ok(HttpResponse::ok()) +} + +fn proxy_signer_request( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + match *request.method() { + Method::GET => { + let (request, _) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ta_proxy_signer_get_request()? + )) + } + Method::POST => { + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let server = request.empty()?; + Ok(HttpResponse::json( + &server.krill().ta_proxy_signer_make_request( + auth.actor() + )? + )) + } + _ => Ok(HttpResponse::method_not_allowed()) + } +} + +async fn proxy_signer_response( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let (server, response) = request.read_json().await?; + server.krill().ta_proxy_signer_process_response(response, auth.actor())?; + Ok(HttpResponse::ok()) +} + +async fn proxy_signer_update( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_post()?; + let (request, auth) = request.proceed_permitted( + Permission::CaAdmin, None + )?; + let (server, info) = request.read_json().await?; + server.krill().ta_proxy_signer_update(info, auth.actor())?; + Ok(HttpResponse::ok()) +} + diff --git a/src/daemon/http/dispatch/testbed.rs b/src/daemon/http/dispatch/testbed.rs new file mode 100644 index 00000000..907d9acc --- /dev/null +++ b/src/daemon/http/dispatch/testbed.rs @@ -0,0 +1,199 @@ +//! `/testbed` +//! +//! Testbed mode enables Krill to run as an open root of a test RPKI hierarchy +//! with web-UI based self-service ability for other RPKI certificate +//! authorities to integrate themselves into the test RPKI hierarchy, both as +//! children whose resources are delegated from the testbed and as publishers +//! into the testbed repository. This feature is very similar to existing +//! web-UI based self-service RPKI test hierarchies such as the RIPE NCC RPKI +//! Test Environment and the APNIC RPKI Testbed. +//! +//! Krill can already do this via a combination of use_ta=true and the +//! existing Krill API _but_ crucially the other RPKI certificate authorities +//! would need to know the Krill API token in order to register themselves +//! with the Krill testbed, giving them far too much power over the testbed. +//! Testbed mode exposes *open* `/testbed/xxx` wrapper API endpoints for +//! exchanging the RFC 8183 XMLs, e.g.: +//! +//! * `/testbed/enabled`: should the web-UI show the testbed UI page? +//! * `/testbed/children`: `` in, `` out +//! * `/testbed/publishers`: `` in, +//! `` out +//! +//! This feature assumes the existence of a built-in "testbed" CA and +//! publisher when testbed mode is enabled. + +use rpki::ca::idexchange::{ChildHandle, PublisherHandle}; +use crate::commons::actor::Actor; +use crate::constants::testbed_ca_handle; +use super::super::request::{Request, PathIter}; +use super::super::response::HttpResponse; +use super::error::DispatchError; + + +//------------ /testbed ------------------------------------------------------ + +pub async fn dispatch( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + if !request.testbed_enabled() { + return Ok(HttpResponse::not_found()) + } + + match path.next() { + Some("enabled") => enabled(request, path), + Some("children") => children(request, path).await, + Some("publishers") => publishers(request, path).await, + _ => Ok(HttpResponse::not_found()) + } +} + + +//------------ /testbed/enabled ---------------------------------------------- + +fn enabled( + request: Request<'_>, + path: PathIter<'_>, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let _server = request.empty()?; + Ok(HttpResponse::ok()) +} + + +//------------ /testbed/children --------------------------------------------- + +async fn children( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.parse_opt_next()? { + None => children_index(request).await, + Some(child) => children_child(request, path, child), + } +} + +async fn children_index( + request: Request<'_> +) -> Result { + request.check_post()?; + let (request, _) = request.proceed_unchecked(); + let (server, child) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().ca_add_child( + &testbed_ca_handle(), child, &Actor::anonymous() + )? + )) +} + +fn children_child( + request: Request<'_>, + mut path: PathIter<'_>, + child: ChildHandle, +) -> Result { + match path.next() { + None => children_child_index(request, child), + Some("parent_response.xml") => { + children_child_response(request, path, child) + } + _ => Ok(HttpResponse::not_found()) + } +} + +fn children_child_index( + request: Request<'_>, + child: ChildHandle, +) -> Result { + request.check_delete()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + server.krill().ca_child_remove( + &testbed_ca_handle(), child, &Actor::anonymous() + )?; + Ok(HttpResponse::ok()) +} + +fn children_child_response( + request: Request<'_>, + path: PathIter<'_>, + child: ChildHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().ca_parent_response( + &testbed_ca_handle(), child + )?.to_xml_vec() + )) +} + + +//------------ /testbed/publishers ------------------------------------------- + +async fn publishers( + request: Request<'_>, + mut path: PathIter<'_>, +) -> Result { + match path.parse_opt_next()? { + None => publishers_index(request).await, + Some(publisher) => publishers_publisher(request, path, publisher), + } +} + +async fn publishers_index( + request: Request<'_> +) -> Result { + request.check_post()?; + let (request, _) = request.proceed_unchecked(); + let (server, pbl) = request.read_json().await?; + Ok(HttpResponse::json( + &server.krill().add_publisher(pbl, &Actor::anonymous())? + )) +} + +fn publishers_publisher( + request: Request<'_>, + mut path: PathIter<'_>, + publisher: PublisherHandle, +) -> Result { + match path.next() { + None => publishers_publisher_index(request, publisher), + Some("response.xml") => { + publishers_publisher_response(request, path, publisher) + } + _ => Ok(HttpResponse::not_found()) + } +} + +fn publishers_publisher_index( + request: Request<'_>, + publisher: PublisherHandle, +) -> Result { + request.check_delete()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + server.krill().remove_publisher( + publisher, &Actor::anonymous() + )?; + Ok(HttpResponse::ok()) +} + +fn publishers_publisher_response( + request: Request<'_>, + path: PathIter<'_>, + publisher: PublisherHandle, +) -> Result { + path.check_exhausted()?; + request.check_get()?; + let (request, _) = request.proceed_unchecked(); + let server = request.empty()?; + Ok(HttpResponse::xml( + server.krill().repository_response(&publisher)?.to_xml_vec() + )) +} + diff --git a/src/daemon/http/mod.rs b/src/daemon/http/mod.rs new file mode 100644 index 00000000..d9dbf13c --- /dev/null +++ b/src/daemon/http/mod.rs @@ -0,0 +1,11 @@ + +pub mod auth; +mod request; +mod response; +pub mod server; +pub mod tls; +pub mod tls_keys; + +mod dispatch; +mod util; + diff --git a/src/daemon/http/request.rs b/src/daemon/http/request.rs new file mode 100644 index 00000000..60101a76 --- /dev/null +++ b/src/daemon/http/request.rs @@ -0,0 +1,527 @@ +//! HTTP requests. + +#![allow(dead_code)] // XXX + +use std::{fmt, str}; +use std::borrow::Cow; +use std::str::FromStr; +use bytes::Bytes; +use http_body_util::{BodyExt, Limited}; +use hyper::Method; +use hyper::body::Body; +use hyper::header::USER_AGENT; +use hyper::http::uri::PathAndQuery; +use percent_encoding::percent_decode; +use rpki::ca::idexchange::MyHandle; +use serde::de::DeserializeOwned; +use crate::api::status::ErrorResponse; +use crate::commons::error::Error; +use crate::config::Config; +use crate::constants::HTTP_USER_AGENT_TRUNCATE; +use super::auth::{AuthInfo, Permission}; +use super::response::HttpResponse; +use super::server::HttpServer; + + +//------------ HyperRequest -------------------------------------------------- + +/// A type alias for the request we receive from Hyper. +pub type HyperRequest = hyper::Request; + + +//------------ Request ------------------------------------------------------- + +/// An enriched request. +pub struct Request<'a> { + /// The underlying raw request. + request: HyperRequest, + + /// The server providing access to Krill itself. + server: &'a HttpServer, + + /// Authentication information for the request. + auth: AuthInfo, + + /// The limits for reading the body of the request. + limits: BodyLimits, +} + +impl<'a> Request<'a> { + /// Creates a request from the various necessary information. + pub fn new( + request: HyperRequest, + server: &'a HttpServer, + auth: AuthInfo, + limits: BodyLimits, + ) ->Self { + Self { request, server, auth, limits } + } + + /// Returns whether testbed mode is enabled. + pub fn testbed_enabled(&self) -> bool { + self.server.krill().testbed_enabled() + } + + /// Returns the method of this request. + pub fn method(&self) -> &Method { + self.request.method() + } + + /// Checks whether the request is a GET or returns an error response. + pub fn check_get(&self) -> Result<(), HttpResponse> { + match *self.request.method() { + Method::GET => Ok(()), + _ => Err(HttpResponse::method_not_allowed()), + } + } + + /// Checks whether the request is a POST or returns an error response. + pub fn check_post(&self) -> Result<(), HttpResponse> { + match *self.request.method() { + Method::POST => Ok(()), + _ => Err(HttpResponse::method_not_allowed()), + } + } + + /// Checks whether the request is a POST or returns an error response. + pub fn check_delete(&self) -> Result<(), HttpResponse> { + match *self.request.method() { + Method::DELETE => Ok(()), + _ => Err(HttpResponse::method_not_allowed()), + } + } + + /* + /// Returns the full URI of the request. + pub fn uri(&self) -> &Uri { + self.request.uri() + } + */ + + /// Returns the current request path. + pub fn path(&self) -> Result { + RequestPath::from_request(self) + } + + /// Returns a reference to the Hyper request. + pub fn hyper(&self) -> &HyperRequest { + &self.request + } + + /* + /// Returns the headers of the request. + pub fn headers(&self) -> &HeaderMap { + self.request.headers() + } + */ + + /// Returns the user agent header if present. + pub fn user_agent(&self) -> Option { + match self.request.headers().get(&USER_AGENT) { + None => None, + Some(value) => value.to_str().ok().map(|s| { + // Note: HeaderValue.to_str() only returns ok in case the + // value is plain ascii so it's safe to + // treat bytes as characters here. + if s.len() > HTTP_USER_AGENT_TRUNCATE { + s[..HTTP_USER_AGENT_TRUNCATE].to_string() + } else { + s.to_string() + } + }), + } + } + + /// Checks for permissions. + /// + /// Returns an appropriate error response if the permissions are not met. + pub fn check_permission( + &self, permission: Permission, resource: Option<&MyHandle> + ) -> Result<(), HttpResponse> { + self.auth.check_permission(permission, resource).map_err(|err| { + HttpResponse::response_from_error(Error::from(err)) + }) + } + + /// Checks the permissions and progresses to the next processing stage. + /// + /// If the authentication information for the request has the given + /// permissions, returns an [`AuthedRequest`] and an [`Actor`] which + /// allow further processing. + /// + /// Otherwise returns an appropriate error response. + pub fn proceed_permitted( + self, + permission: Permission, + resource: Option<&MyHandle>, + ) -> Result<(AuthedRequest<'a>, AuthInfo), HttpResponse> { + self.check_permission(permission, resource)?; + Ok(( + AuthedRequest { + request: self.request, + server: self.server, + limits: self.limits, + }, + self.auth + )) + } + + /// Permits the request to the next processing stage. + /// + /// Returns [`AuthedRequest`] and [`Actor`] without requiring any + /// permissions whatsoever. + pub fn proceed_unchecked( + self + ) -> (AuthedRequest<'a>, AuthInfo) { + ( + AuthedRequest { + request: self.request, + server: self.server, + limits: self.limits, + }, + self.auth + ) + } + + /// Splits the request into the server and raw Hyper request. + pub fn proceed_raw(self) -> (&'a HttpServer, HyperRequest) { + (self.server, self.request) + } +} + + +//------------ AuthedRequest ------------------------------------------------- + +/// A request that has been checked for the correct access permissions. +/// +/// This type allows access to the request’s body and, by way of reading the +/// body or forcing it to be empty, to the server. +pub struct AuthedRequest<'a> { + /// The underlying raw request. + request: HyperRequest, + + /// The server providing access to Krill itself. + server: &'a HttpServer, + + /// The limits for reading the body of the request. + limits: BodyLimits, +} + +impl<'a> AuthedRequest<'a> { + /// Ensures the body is empty. + pub fn empty(self) -> Result<&'a HttpServer, Error> { + if self.request.body().size_hint().upper() != Some(0) { + return Err(Error::UnexpectedBody) + } + Ok(self.server) + } + + /// Returns the raw bytes of the request body. + pub async fn read_bytes(self) -> Result<(&'a HttpServer, Bytes), Error> { + let limit = self.limits.post_limit_api; + self.read_body(limit).await + } + + /// Get a json object from a post body + pub async fn read_json( + self + ) -> Result<(&'a HttpServer, T), Error> { + let (server, bytes) = self.read_bytes().await?; + let json = serde_json::from_slice(&bytes).map_err(Error::JsonError)?; + Ok((server, json)) + } + + /// Returns the raw bytes of a provisioning protocol request. + pub async fn read_rfc6492_bytes( + self + ) -> Result<(&'a HttpServer, Bytes), Error> { + let limit = self.limits.post_limit_rfc6492; + self.read_body(limit).await + } + + pub async fn read_rfc8181_bytes( + self + ) -> Result<(&'a HttpServer, Bytes), Error> { + let limit = self.limits.post_limit_rfc8181; + self.read_body(limit).await + } + + async fn read_body( + self, limit: u64 + ) -> Result<(&'a HttpServer, Bytes), Error> { + // We’re going to cheat a bit. If we know the body is too big from + // the Content-Length header, we return Error::PostTooBig. But if + // we don’t -- which means there are multiple chunks or somesuch -- + // we just use http_body_utils::Limited and return PostCannotRead + // on any error. + + if self.request.body().size_hint().lower() > limit { + return Err(Error::PostTooBig); + } + + Ok(( + self.server, + Limited::new( + self.request.into_body(), + limit.try_into().unwrap_or(usize::MAX), + ).collect().await.map_err(|_| { + Error::PostCannotRead + })?.to_bytes() + )) + } +} + + +//------------ RequestPath --------------------------------------------------- + +/// The path of a request’s URI. +/// +/// It primarily allows iterating over the path segments. Note that because it +/// needs to be a “borrowing iterator,” it cannot implement the normal +/// `Iterator` trait. +#[derive(Debug, Clone)] +pub struct RequestPath { + path: Result, +} + +impl RequestPath { + fn from_request(request: &Request) -> Result { + let path = if let Cow::Owned(some) = percent_decode( + request.request.uri().path().as_bytes() + ).decode_utf8().map_err(|_| InvalidPath)? { + Err(some) + } + else { + Ok( + request.request.uri().path_and_query() + .ok_or(InvalidPath)?.clone() + ) + }; + Ok(Self { path }) + } + + pub fn as_str(&self) -> &str { + match self.path.as_ref() { + Ok(path) => path.path(), + Err(path) => path.as_str() + } + } + + pub fn iter(&self) -> PathIter { + PathIter::new(self.as_str()) + } +} + +impl AsRef for RequestPath { + fn as_ref(&self) -> &str { + self.as_str() + } +} + + +//------------ PathIter ------------------------------------------------- + +#[derive(Debug)] +pub struct PathIter<'a> { + full: &'a str, + remaining: Option<&'a str>, +} + +impl<'a> PathIter<'a> { + fn new(path: &'a str) -> Self { + Self { + full: path, + remaining: Some(path.strip_prefix('/').unwrap_or(path)) + } + } + + /// Returns a copy with a possible trailing slash removed. + pub fn strip_trailing_slash(&self) -> Self { + // Some("") means there _was_ a trailing slash and we are now just + // past it. So we need to transform this case into an exhausted path. + let remaining = match self.remaining { + Some("") | None => None, + Some(remaining) => { + Some(remaining.strip_suffix('/').unwrap_or(remaining)) + } + }; + Self { + full: self.full.strip_suffix('/').unwrap_or(self.full), + remaining + } + } + + pub fn full(&self) -> &str { + self.full + } + + pub fn remaining(&self) -> Option<&str> { + self.remaining + } + + /// Checks that the path has been exhausted. + /// + /// Returns a 404 error response if it isn’t. + pub fn check_exhausted(&self) -> Result<(), HttpResponse> { + if self.remaining.is_some() { + Err(HttpResponse::not_found()) + } + else { + Ok(()) + } + } + + /// Parses the next segment as the given type or returns a Not Found. + pub fn parse_next(&mut self) -> Result { + T::from_str( + self.next().ok_or_else(HttpResponse::not_found)? + ).map_err(|_| { + HttpResponse::not_found() + }) + } + + /// Parses the next optional segment. + /// + /// Returns `Ok(None)` if we reached the end of the path. Returns a + /// Not Found error response if parsing failed. + pub fn parse_opt_next( + &mut self + ) -> Result, HttpResponse> { + self.next().map(|s| { + T::from_str(s).map_err(|_| HttpResponse::not_found()) + }).transpose() + } + + /// Parses the next optional segment, allowing for a trailing slash. + /// + /// Returns `Ok(None)` if we reached the end of the path or if there was + /// a trailing slash. Returns a/ Not Found error response if parsing + /// failed. + pub fn parse_opt_next_trailing_slash( + &mut self + ) -> Result, HttpResponse> { + self.next().map(|s| { + T::from_str(s).map_err(|_| HttpResponse::not_found()) + }).transpose() + } +} + +impl<'a> Iterator for PathIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + let remaining = self.remaining?; + let slash = match remaining.find('/') { + Some(pos) => pos, + None => { + let res = remaining; + self.remaining = None; + return Some(res) + } + }; + let res = &remaining[..slash]; + self.remaining = Some(&remaining[slash + 1..]); + Some(res) + } +} + + +//------------ BodyLimits ---------------------------------------------------- + +/// The size limits of a request body. +#[derive(Clone, Copy, Debug)] +pub struct BodyLimits { + /// The POST limit for API data. + post_limit_api: u64, + + /// The POST limit for provisioning protocol data. + post_limit_rfc6492: u64, + + + /// The POST limit for publication protocol data. + post_limit_rfc8181: u64, +} + +impl BodyLimits { + /// Creates the limits from the config. + pub fn from_config(config: &Config) -> Self { + Self { + post_limit_api: config.post_limit_api, + post_limit_rfc6492: config.post_limit_rfc6492, + post_limit_rfc8181: config.post_limit_rfc8181, + } + } +} + + +//------------ InvalidPath --------------------------------------------------- + +/// An error happened while preparing the request path. +#[derive(Clone, Copy, Debug)] +pub struct InvalidPath; + +impl From for ErrorResponse { + fn from(_: InvalidPath) -> Self { + Self::new("invalid-path", "The request path was invalid.") + } +} + +impl fmt::Display for InvalidPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("invalid request path") + } +} + + +//============ Tests ========================================================= + +#[cfg(test)] +mod test { + use super::*; + + impl RequestPath { + fn test_str(s: &str) -> Self { + Self { + path: Err( + percent_decode( + s.as_bytes() + ).decode_utf8().unwrap().into_owned() + ) + } + } + } + + #[test] + fn request_path_next() { + let path = RequestPath::test_str("/foo/bar/baz/"); + let mut path = path.iter(); + assert_eq!(path.next(), Some("foo")); + assert_eq!(path.next(), Some("bar")); + assert_eq!(path.next(), Some("baz")); + assert_eq!(path.next(), Some("")); + assert_eq!(path.next(), None); + + let path = RequestPath::test_str("/foo/bar/baz"); + let mut path = path.iter(); + assert_eq!(path.next(), Some("foo")); + assert_eq!(path.next(), Some("bar")); + assert_eq!(path.next(), Some("baz")); + assert_eq!(path.next(), None); + + let path = RequestPath::test_str("/foo/b%61%72%2fbaz/"); + let mut path = path.iter(); + assert_eq!(path.next(), Some("foo")); + assert_eq!(path.next(), Some("bar")); + assert_eq!(path.next(), Some("baz")); + assert_eq!(path.next(), Some("")); + assert_eq!(path.next(), None); + + let path = RequestPath::test_str("/foö/bär/baß"); + let mut path = path.iter(); + assert_eq!(path.next(), Some("foö")); + assert_eq!(path.next(), Some("bär")); + assert_eq!(path.next(), Some("baß")); + assert_eq!(path.next(), None); + } +} + diff --git a/src/server/http/response.rs b/src/daemon/http/response.rs similarity index 73% rename from src/server/http/response.rs rename to src/daemon/http/response.rs index 806d9e52..9f6c8775 100644 --- a/src/server/http/response.rs +++ b/src/daemon/http/response.rs @@ -1,15 +1,13 @@ -use std::io; use bytes::Bytes; use http_body_util::{Either, Empty, Full}; use hyper::{HeaderMap, StatusCode}; +use hyper::header::{HeaderName, HeaderValue}; +use log::warn; use rpki::ca::{provisioning, publication}; use serde::Serialize; - -use crate::{ - commons::{ - error::Error, - }, -}; +use crate::api::admin::Token; +use crate::api::status::ErrorResponse; +use crate::commons::error::Error; //----------- ContentType ---------------------------------------------------- @@ -32,8 +30,8 @@ enum ContentType { Woff2, } -impl AsRef for ContentType { - fn as_ref(&self) -> &str { +impl ContentType { + fn as_str(&self) -> &'static str { match self { ContentType::Cert => "application/x-x509-ca-cert", ContentType::Json => "application/json", @@ -63,9 +61,9 @@ pub type HyperResponse = hyper::Response; struct Response { status: StatusCode, - content_type: ContentType, + content_type: &'static str, max_age: Option, - body: Vec, + body: Bytes, cause: Option, } @@ -73,9 +71,9 @@ impl Response { fn new(status: StatusCode) -> Self { Response { status, - content_type: ContentType::Text, + content_type: ContentType::Text.as_str(), max_age: None, - body: Vec::new(), + body: Bytes::default(), cause: None, } } @@ -83,7 +81,7 @@ impl Response { fn finalize(self) -> HttpResponse { let mut builder = hyper::Response::builder() .status(self.status) - .header("Content-Type", self.content_type.as_ref()); + .header("Content-Type", self.content_type); if let Some(max_age) = self.max_age { builder = builder @@ -97,7 +95,7 @@ impl Response { let body = if self.body.is_empty() { Either::Left(Empty::new()) } else { - Either::Right(Full::new(self.body.into())) + Either::Right(Full::new(self.body)) }; let response = builder.body(body).unwrap(); @@ -115,18 +113,10 @@ impl From for HttpResponse { } } -impl io::Write for Response { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.body.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.body.flush() - } -} //------------ HttpResponse -------------------------------------------------- +#[derive(Debug)] pub struct HttpResponse { response: HyperResponse, cause: Option, @@ -148,6 +138,10 @@ impl HttpResponse { self.response } + pub fn into_hyper(self) -> HyperResponse { + self.response + } + pub fn loggable(&self) -> bool { self.loggable } @@ -194,27 +188,38 @@ impl HttpResponse { self.response.headers() } - fn ok_response(content_type: ContentType, body: Vec) -> Self { + pub fn ok_with_body( + content_type: &'static str, + body: impl Into + ) -> Self { Response { status: StatusCode::OK, content_type, max_age: None, - body, + body: body.into(), cause: None, } .finalize() } + + fn ok_response( + content_type: ContentType, + body: impl Into + ) -> Self { + Self::ok_with_body(content_type.as_str(), body) + } + pub fn json(object: &O) -> Self { match serde_json::to_string(object) { Ok(json) => { - Self::ok_response(ContentType::Json, json.into_bytes()) + Self::ok_response(ContentType::Json, json) } Err(e) => Self::response_from_error(Error::JsonError(e)), } } - pub fn text(body: Vec) -> Self { + pub fn text(body: impl Into) -> Self { Self::ok_response(ContentType::Text, body) } @@ -222,7 +227,7 @@ impl HttpResponse { HttpResponse::new( hyper::Response::builder() .status(StatusCode::OK) - .header("Content-Type", ContentType::Text.as_ref()) + .header("Content-Type", ContentType::Text.as_str()) .header("Cache-Control", "no-cache") .body(Either::Right(Full::new(body.into()))) .unwrap(), @@ -240,23 +245,23 @@ impl HttpResponse { pub fn xml_with_cache(body: Vec, seconds: usize) -> Self { Response { status: StatusCode::OK, - content_type: ContentType::Xml, + content_type: ContentType::Xml.as_str(), max_age: Some(seconds), - body, + body: body.into(), cause: None, } .finalize() } - pub fn rfc8181(body: Vec) -> Self { + pub fn rfc8181(body: Bytes) -> Self { Self::ok_response(ContentType::Rfc8181, body) } - pub fn rfc6492(body: Vec) -> Self { + pub fn rfc6492(body: Bytes) -> Self { Self::ok_response(ContentType::Rfc6492, body) } - pub fn cert(body: Vec) -> Self { + pub fn cert(body: Bytes) -> Self { Self::ok_response(ContentType::Cert, body) } @@ -288,18 +293,31 @@ impl HttpResponse { Self::ok_response(ContentType::Woff2, content.to_vec()) } + pub fn error( + status: StatusCode, error: impl Into + ) -> Self { + let error = error.into(); + let body = serde_json::to_string(&error).unwrap().into(); + Response { + status, + content_type: ContentType::Json.as_str(), + max_age: None, + body, + cause: None, + }.finalize() + } + pub fn response_from_error(error: Error) -> Self { let status = error.status(); let response = error.to_error_response(); - let body = serde_json::to_string(&response).unwrap(); + let body = serde_json::to_string(&response).unwrap().into(); Response { status, - content_type: ContentType::Json, + content_type: ContentType::Json.as_str(), max_age: None, - body: body.into_bytes(), + body, cause: Some(error), - } - .finalize() + }.finalize() } pub fn ok() -> Self { @@ -327,5 +345,33 @@ impl HttpResponse { pub fn forbidden(err: String) -> Self { Self::response_from_error(Error::ApiInsufficientRights(err)) } + + pub fn method_not_allowed() -> Self { + Response::new(StatusCode::METHOD_NOT_ALLOWED).finalize() + } + + // 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. + pub fn add_authorization_token( + &mut self, token: Token, + ) { + let header_name = const { HeaderName::from_static("authorization") }; + let header_value = match HeaderValue::from_maybe_shared( + Bytes::from(format!("Bearer {}", &token)) + ) { + Ok(value) => value, + Err(_) => { + warn!( + "Internal error: unable to add refreshed auth token \ + '{token}' to the response." + ); + return + } + }; + + self.response.headers_mut().insert(header_name, header_value); + } } diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs new file mode 100644 index 00000000..fe5c3626 --- /dev/null +++ b/src/daemon/http/server.rs @@ -0,0 +1,185 @@ +use std::env; +use std::sync::Arc; +use clap::crate_version; +use hyper::StatusCode; +use log::{error, info, warn, trace}; +use crate::api::admin::ServerInfo; +use crate::api::ca::Timestamp; +use crate::commons::KrillResult; +use crate::commons::error::FatalError; +use crate::config::Config; +use crate::constants::KRILL_ENV_HTTP_LOG_INFO; +use crate::server::manager::KrillManager; +use super::auth::Authorizer; +use super::dispatch::{DispatchError, dispatch_request}; +use super::request::{BodyLimits, HyperRequest, Request}; +use super::response::{HyperResponse, HttpResponse}; + + + +//------------ HttpServer ---------------------------------------------------- + +/// The Krill HTTP server. +pub struct HttpServer { + /// The Krill “business logic.” + krill: KrillManager, + + /// The component responsible for API authorization checks + authorizer: Authorizer, + + /// A copy of the configuration. + config: Arc, + + /// Time this server was started + started: Timestamp, +} + +impl HttpServer { + /// Creates a new server from a Krill manager and the configuration. + pub fn new( + krill: KrillManager, + config: Arc + ) -> KrillResult> { + Ok(Self { + krill, + authorizer: Authorizer::new(config.clone())?, + config, + started: Timestamp::now(), + }.into()) + } + + /// Processes an HTTP request. + pub async fn process_request( + &self, request: HyperRequest + ) -> Result { + let logger = RequestLogger::begin(&request); + let (auth, new_token) = self.authorizer.authenticate_request( + &request + ).await; + let request = Request::new( + request, self, auth, BodyLimits::from_config(&self.config) + ); + let path = match request.path() { + Ok(path) => path, + Err(err) => { + return Ok( + HttpResponse::error( + StatusCode::BAD_REQUEST, err + ).into_hyper() + ); + } + }; + + let mut response = match dispatch_request( + request, path.iter(), + ).await { + Ok(response) => Ok(response), + Err(DispatchError::Response(response)) => Ok(response), + Err(DispatchError::Fatal(err)) => Err(err), + }; + + // Augment the response with any updated auth details that were + // determined above. + if let (Ok(response), Some(token)) = (response.as_mut(), new_token) { + response.add_authorization_token(token); + } + + logger.end(response.as_ref()); + response.map(HttpResponse::into_hyper) + } +} + +impl HttpServer { + /// Returns a reference to the Krill manager. + pub(super) fn krill(&self) -> &KrillManager { + &self.krill + } + + /// Returns a reference to the authorizer. + pub(super) fn authorizer(&self) -> &Authorizer { + &self.authorizer + } + + /// Returns a reference to the configuration. + pub(super) fn config(&self) -> &Config { + &self.config + } + + pub(super) fn server_info(&self) -> ServerInfo { + ServerInfo { version: crate_version!().into(), started: self.started } + } +} + + +//------------ RequestLogger ------------------------------------------------- + +struct RequestLogger { + req_method: hyper::Method, + req_path: String, +} + +impl RequestLogger { + fn begin(req: &HyperRequest) -> Self { + let req_method = req.method().clone(); + let req_path = req.uri().path().into(); + + trace!( + "Request: method={} path={} headers={:?}", + &req_method, + &req_path, + &req.headers() + ); + + RequestLogger { + req_method, + req_path, + } + } + + fn end(&self, res: Result<&HttpResponse, &FatalError>) { + 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() + ); + } + + if response.loggable() { + trace!( + "{} {} {}", + self.req_method, + self.req_path, + response.status() + ); + trace!( + "Response: headers={:?} body={:?}", + response.headers(), + response.body() + ); + } + } + Err(err) => { + error!( + "{} {} Fatal error: {}", + self.req_method, self.req_path, err + ); + } + } + } +} + diff --git a/src/server/http/tls.rs b/src/daemon/http/tls.rs similarity index 100% rename from src/server/http/tls.rs rename to src/daemon/http/tls.rs diff --git a/src/server/http/tls_keys.rs b/src/daemon/http/tls_keys.rs similarity index 100% rename from src/server/http/tls_keys.rs rename to src/daemon/http/tls_keys.rs diff --git a/src/daemon/http/util.rs b/src/daemon/http/util.rs new file mode 100644 index 00000000..16900d4d --- /dev/null +++ b/src/daemon/http/util.rs @@ -0,0 +1,14 @@ +//! Utils for the HTTP service. +//! +//! This is a temporary place to park some things until we find a better home +//! for them. + +#[cfg(feature = "multi-user")] +pub fn url_encode>( + s: S +) -> Result { + urlparse::quote(s, b"").map_err(|err| { + crate::commons::error::Error::custom(err.to_string()) + }) +} + diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs new file mode 100644 index 00000000..04c2f5eb --- /dev/null +++ b/src/daemon/mod.rs @@ -0,0 +1,8 @@ +//! The Krill daemon. +//! +//! This module contains the code actually driving the daemon including +//! processing HTTP requests for the API. + +pub mod http; +pub mod start; + diff --git a/src/daemon/start.rs b/src/daemon/start.rs new file mode 100644 index 00000000..12a5ce16 --- /dev/null +++ b/src/daemon/start.rs @@ -0,0 +1,247 @@ +use std::{env, process}; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; +use log::error; +use hyper::service::service_fn; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use tokio::select; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tokio_rustls::TlsAcceptor; +use crate::commons::file; +use crate::commons::error::Error; +use crate::config::Config; +use crate::constants::KRILL_ENV_UPGRADE_ONLY; +use crate::server::properties::PropertiesManager; +use crate::server::manager::KrillManager; +use crate::upgrades::{ + finalise_data_migration, post_start_upgrade, + prepare_upgrade_data_migrations, UpgradeError, UpgradeMode, +}; +use super::http::{tls, tls_keys}; +use super::http::server::HttpServer; + + +pub async fn start_krill_daemon( + config: Arc, + mut signal_running: Option>, +) -> Result<(), Error> { + write_pid_file_or_die(&config); + test_data_dirs_or_die(&config); + + // Set up the runtime properties manager, so that we can check + // the version used for the current data in storage + let properties_manager = PropertiesManager::create( + &config.storage_uri, + config.use_history_cache, + )?; + + // Call upgrade, this will only do actual work if needed. + let upgrade_report = prepare_upgrade_data_migrations( + UpgradeMode::PrepareToFinalise, &config, &properties_manager + ).map_err(|e| { + match e { + UpgradeError::CodeOlderThanData(_,_) => { + Error::Custom(e.to_string()) + }, + _ => { + Error::Custom(format!( + "Upgrade data migration failed with error: {}\n\n\ + NOTE: your data was not changed. Please downgrade \ + your krill instance to your previous version.", + e + )) + } + } + })?; + + if let Some(report) = &upgrade_report { + finalise_data_migration( + report.versions(), &config, &properties_manager + ).map_err(|e| { + Error::Custom(format!( + "Finishing prepared migration failed unexpectedly. Please \ + check your data {}. 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.storage_uri, + report.versions().from(), + report.versions().from(), + report.versions().from(), + e + )) + })?; + } + + // Create the Krill manager, this will create the necessary data + // sub-directories if needed + let krill = KrillManager::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, &krill).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.build_scheduler(); + let scheduler_future = scheduler.run(); + + // Create the HTTP server. + let server = HttpServer::new(krill, config.clone())?; + + // 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.tls_keys_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( + server.clone(), + socket_addr, + config.clone(), + signal_running.take(), + )) + }), + ); + + select!( + _ = server_futures => error!("http server stopped unexpectedly"), + _ = scheduler_future => error!("scheduler stopped unexpectedly"), + ); + + Err(Error::custom("stopping krill process")) +} + +/// Runs an HTTP listener on a single socket. +async fn single_http_listener( + server: Arc, + addr: SocketAddr, + config: Arc, + signal_running: Option>, +) { + let listener = match TcpListener::bind(addr).await { + Ok(listener) => listener, + Err(err) => { + error!("Could not bind to {}: {}", addr, err); + return; + } + }; + + let tls = if config.https_mode().is_disable_https() { + None + } else { + match tls::create_server_config( + &tls_keys::key_file_path(config.tls_keys_dir()), + &tls_keys::cert_file_path(config.tls_keys_dir()), + ) { + Ok(config) => Some(TlsAcceptor::from(Arc::new(config))), + Err(err) => { + error!("{}", err); + return; + } + } + }; + + if let Some(tx) = signal_running { + let _ = tx.send(()); + } + + loop { + let stream = match listener.accept().await { + Ok((stream, _addr)) => { + tls::MaybeTlsTcpStream::new(stream, tls.as_ref()) + } + Err(err) => { + error!("Fatal error in HTTP server {}: {}", addr, err); + return; + } + }; + let server = server.clone(); + tokio::task::spawn(async move { + let _ = hyper_util::server::conn::auto::Builder::new( + TokioExecutor::new(), + ) + .serve_connection( + TokioIo::new(stream), + service_fn(move |req| { + let server = server.clone(); + async move { server.process_request(req).await } + }), + ) + .await; + }); + } +} + +fn write_pid_file_or_die(config: &Config) { + if let Err(e) = file::save( + process::id().to_string().as_bytes(), config.pid_file() + ) { + print_write_error_hint_and_die(format!( + "Could not write PID file: {}", + e + )); + } +} + +fn test_data_dirs_or_die(config: &Config) { + test_data_dir_or_die("tls_keys_dir", config.tls_keys_dir()); + test_data_dir_or_die("repo_dir", config.repo_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); + } +} + +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 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"); + eprintln!("ReadWritePaths, the easiest way may be by doing "); + eprintln!("'systemctl edit krill' and add a section like:"); + eprintln!(); + eprintln!("[Service]"); + eprintln!("ReadWritePaths=/local/path1 /local/path2 ..."); +} + diff --git a/src/lib.rs b/src/lib.rs index d96754e2..47a6aa08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,9 @@ pub mod api; pub mod cli; pub mod commons; +pub mod config; pub mod constants; +pub mod daemon; pub mod server; pub mod tasigner; pub mod upgrades; diff --git a/src/server/ca/aspa.rs b/src/server/ca/aspa.rs index 4531b773..3bea7b92 100644 --- a/src/server/ca/aspa.rs +++ b/src/server/ca/aspa.rs @@ -17,7 +17,7 @@ use crate::api::ca::ObjectName; use crate::commons::KrillResult; use crate::commons::crypto::KrillSigner; use crate::commons::error::Error; -use crate::server::config::{Config, IssuanceTimingConfig}; +use crate::config::{Config, IssuanceTimingConfig}; use super::events::CertAuthEvent; use super::keys::CertifiedKey; diff --git a/src/server/ca/bgpsec.rs b/src/server/ca/bgpsec.rs index 4dc0f713..44fbecce 100644 --- a/src/server/ca/bgpsec.rs +++ b/src/server/ca/bgpsec.rs @@ -19,7 +19,7 @@ use crate::api::ca::ObjectName; use crate::commons::KrillResult; use crate::commons::error::Error; use crate::commons::crypto::KrillSigner; -use crate::server::config::{Config, IssuanceTimingConfig}; +use crate::config::{Config, IssuanceTimingConfig}; use super::events::CertAuthEvent; use super::keys::CertifiedKey; diff --git a/src/server/ca/certauth.rs b/src/server/ca/certauth.rs index 6b8f2862..50694550 100644 --- a/src/server/ca/certauth.rs +++ b/src/server/ca/certauth.rs @@ -47,7 +47,7 @@ use crate::commons::crypto::{CsrInfo, KrillSigner}; use crate::commons::error::Error; use crate::commons::eventsourcing::Aggregate; use crate::constants::test_mode_enabled; -use crate::server::config::{Config, IssuanceTimingConfig}; +use crate::config::{Config, IssuanceTimingConfig}; use super::aspa::AspaDefinitions; use super::bgpsec::BgpSecDefinitions; use super::child::{ChildDetails, ChildCertificateUpdates, UsedKeyState}; @@ -2744,7 +2744,7 @@ impl Rfc8183Id { mod tests { use crate::commons::crypto::KrillSignerBuilder; use crate::commons::test; - use crate::server::config::ConfigDefaults; + use crate::config::ConfigDefaults; use std::time::Duration; use super::*; diff --git a/src/server/ca/child.rs b/src/server/ca/child.rs index a2195e99..f6ba9dab 100644 --- a/src/server/ca/child.rs +++ b/src/server/ca/child.rs @@ -12,7 +12,7 @@ use crate::api::ca::{ use crate::commons::KrillResult; use crate::commons::crypto::{KrillSigner, SignSupport}; use crate::commons::error::Error; -use crate::server::config::IssuanceTimingConfig; +use crate::config::IssuanceTimingConfig; //------------ UsedKeyState -------------------------------------------------- diff --git a/src/server/ca/commands.rs b/src/server/ca/commands.rs index d4570821..9f28cd5d 100644 --- a/src/server/ca/commands.rs +++ b/src/server/ca/commands.rs @@ -34,7 +34,7 @@ use crate::commons::eventsourcing::{ self, InitCommandDetails, SentCommand, SentInitCommand, WithStorableDetails, }; -use crate::server::config::Config; +use crate::config::Config; use super::events::CertAuthEvent; use super::rc::DropReason; diff --git a/src/server/ca/manager.rs b/src/server/ca/manager.rs index fee7b050..b323fd79 100644 --- a/src/server/ca/manager.rs +++ b/src/server/ca/manager.rs @@ -58,8 +58,8 @@ use crate::constants::{ CASERVER_NS, STATUS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, TA_NAME, ta_handle, }; -use crate::server::http::auth::{AuthInfo, Permission}; // XXX remove -use crate::server::config::Config; +use crate::daemon::http::auth::{AuthInfo, Permission}; // XXX remove +use crate::config::Config; use crate::server::mq::{now, Task, TaskQueue}; use crate::server::pubd::RepositoryManager; use crate::server::taproxy::{ @@ -77,18 +77,6 @@ use super::publishing::{CaObjectsStore, DeprecatedRepository}; use super::status::{CaStatus, CaStatusStore}; -//------------ testbed_ca_handle --------------------------------------------- - -/// The handle of the CA used by the testbed. -pub const TESTBED_CA_NAME: &str = "testbed"; - -/// Returns the CA handle for the testbed. -pub fn testbed_ca_handle() -> CaHandle { - use std::str::FromStr; - CaHandle::from_str(TESTBED_CA_NAME).unwrap() -} - - //------------ CaManager ----------------------------------------------------- /// Manages access to all CAs. @@ -304,7 +292,7 @@ impl CaManager { /// those objects that are close to expiring. /// /// Returns all CAs for which objects were republished. - pub async fn republish_all( + pub fn republish_all( &self, force: bool, ) -> KrillResult> { @@ -657,7 +645,7 @@ impl CaManager { actor: &Actor, ) -> KrillResult<()> { self.process_ca_command( - handle.clone(), actor, + handle, actor, CertAuthCommandDetails::GenerateNewIdKey( self.signer.clone(), ) @@ -671,8 +659,6 @@ impl CaManager { } /// Returns the CAs that the given policy allows read access to. - // - // XXX This should probably not live here but in krillserver. pub fn ca_list( &self, auth: &AuthInfo, ) -> KrillResult { @@ -2531,10 +2517,10 @@ impl CaManager { impl CaManager { /// Schedules synchronizing all CAs with their repositories. pub fn cas_schedule_repo_sync_all( - &self, auth: &AuthInfo, + &self, ) -> KrillResult<()> { - for ca in &self.ca_list(auth)?.cas { - self.cas_schedule_repo_sync(ca.handle.clone())?; + for ca in self.ca_handles()? { + self.cas_schedule_repo_sync(ca)?; } Ok(()) } @@ -3011,9 +2997,9 @@ impl CaManager { /// Returns the current ASPA definitions for this CA. pub fn ca_aspas_definitions_show( &self, - ca: CaHandle, + ca: &CaHandle, ) -> KrillResult { - Ok(self.get_ca(&ca)?.aspas_definitions_show()) + Ok(self.get_ca(ca)?.aspas_definitions_show()) } /// Adds a new ASPA definition for this CA. @@ -3024,7 +3010,7 @@ impl CaManager { actor: &Actor, ) -> KrillResult<()> { self.process_ca_command( - ca.clone(), actor, + ca, actor, CertAuthCommandDetails::AspasUpdate( updates, self.config.clone(), @@ -3060,9 +3046,9 @@ impl CaManager { /// Returns the BGPsec definitions for a CA. pub fn ca_bgpsec_definitions_show( &self, - ca: CaHandle, + ca: &CaHandle, ) -> KrillResult { - Ok(self.get_ca(&ca)?.bgpsec_definitions_show()) + Ok(self.get_ca(ca)?.bgpsec_definitions_show()) } /// Updates the BGPsec definitions for a CA. diff --git a/src/server/ca/mod.rs b/src/server/ca/mod.rs index 6670c9ba..33688152 100644 --- a/src/server/ca/mod.rs +++ b/src/server/ca/mod.rs @@ -16,7 +16,6 @@ mod status; pub mod upgrades; pub use self::manager::CaManager; -pub use self::manager::testbed_ca_handle; pub use self::status::CaStatusStore; pub use self::status::CaStatus; diff --git a/src/server/ca/publishing.rs b/src/server/ca/publishing.rs index 4500cee8..b735cb00 100644 --- a/src/server/ca/publishing.rs +++ b/src/server/ca/publishing.rs @@ -28,7 +28,7 @@ use crate::commons::error::Error; use crate::commons::eventsourcing::PreSaveEventListener; use crate::commons::storage::{Key, KeyValueStore, Scope, Segment}; use crate::constants::CA_OBJECTS_NS; -use crate::server::config::IssuanceTimingConfig; +use crate::config::IssuanceTimingConfig; use super::aspa::{AspaInfo, AspaObjectsUpdates}; use super::bgpsec::{BgpSecCertInfo, BgpSecCertificateUpdates}; use super::certauth::CertAuth; diff --git a/src/server/ca/rc.rs b/src/server/ca/rc.rs index 3c817bab..bdfc89cc 100644 --- a/src/server/ca/rc.rs +++ b/src/server/ca/rc.rs @@ -20,7 +20,7 @@ use crate::api::roa::{RoaConfiguration, RoaInfo}; use crate::commons::KrillResult; use crate::commons::crypto::{CsrInfo, KrillSigner, SignSupport}; use crate::commons::error::{Error, KrillError}; -use crate::server::config::{Config, IssuanceTimingConfig}; +use crate::config::{Config, IssuanceTimingConfig}; use super::aspa::{AspaDefinitions, AspaObjects, AspaObjectsUpdates}; use super::bgpsec::{ BgpSecCertificates, BgpSecCertificateUpdates, BgpSecDefinitions diff --git a/src/server/ca/roa.rs b/src/server/ca/roa.rs index c6511ffb..ed4b6adb 100644 --- a/src/server/ca/roa.rs +++ b/src/server/ca/roa.rs @@ -20,7 +20,7 @@ use crate::api::roa::{ use crate::commons::KrillResult; use crate::commons::crypto::KrillSigner; use crate::commons::error::{Error, RoaDeltaError}; -use crate::server::config::{Config, IssuanceTimingConfig}; +use crate::config::{Config, IssuanceTimingConfig}; use super::events::CertAuthEvent; use super::keys::CertifiedKey; diff --git a/src/server/ca/status.rs b/src/server/ca/status.rs index 82594b4d..8f428ab8 100644 --- a/src/server/ca/status.rs +++ b/src/server/ca/status.rs @@ -569,11 +569,21 @@ impl CaStatus { &self.repo } + /// Converts the status into the repository status. + pub fn into_repo(self) -> RepoStatus { + self.repo + } + /// Returns a reference to the parent statuses. pub fn parents(&self) -> &ParentStatuses { &self.parents } + /// Converts the status into the parent statuses. + pub fn into_parents(self) -> ParentStatuses { + self.parents + } + /// Returns a reference to the child statuses. pub fn children(&self) -> &HashMap { &self.children diff --git a/src/server/ca/upgrades/data_migration.rs b/src/server/ca/upgrades/data_migration.rs index 74206f6e..7f4d0a81 100644 --- a/src/server/ca/upgrades/data_migration.rs +++ b/src/server/ca/upgrades/data_migration.rs @@ -4,7 +4,7 @@ use crate::commons::crypto::KrillSignerBuilder; use crate::constants::CASERVER_NS; use crate::server::ca::certauth::CertAuth; use crate::server::ca::publishing::CaObjectsStore; -use crate::server::config::Config; +use crate::config::Config; use crate::upgrades::UpgradeResult; use crate::upgrades::data_migration::check_agg_store; diff --git a/src/server/ca/upgrades/pre_0_10_0/migration.rs b/src/server/ca/upgrades/pre_0_10_0/migration.rs index 88775a99..5fb3cfdf 100644 --- a/src/server/ca/upgrades/pre_0_10_0/migration.rs +++ b/src/server/ca/upgrades/pre_0_10_0/migration.rs @@ -13,7 +13,7 @@ use crate::server::ca::certauth::CertAuth; use crate::server::ca::commands::CertAuthStorableCommand; use crate::server::ca::events::{CertAuthEvent, CertAuthInitEvent}; use crate::server::ca::publishing::CaObjects; -use crate::server::config::Config; +use crate::config::Config; use crate::upgrades::{ AspaMigrationConfigUpdates, AspaMigrationConfigs, CommandMigrationEffect, UpgradeAggregateStorePre0_14, UpgradeError, UpgradeMode, UpgradeResult, diff --git a/src/server/ca/upgrades/pre_0_10_0/old_events.rs b/src/server/ca/upgrades/pre_0_10_0/old_events.rs index 1127f410..e11e9d5e 100644 --- a/src/server/ca/upgrades/pre_0_10_0/old_events.rs +++ b/src/server/ca/upgrades/pre_0_10_0/old_events.rs @@ -136,7 +136,7 @@ impl TryFrom for TaCertDetails { let tal = TrustAnchorLocator::new(tal.uris, rsync_uri, &public_key); - Ok(TaCertDetails::new(rvcd_cert, tal)) + Ok(TaCertDetails { cert: rvcd_cert, tal }) } } diff --git a/src/server/ca/upgrades/pre_0_14_0/migration.rs b/src/server/ca/upgrades/pre_0_14_0/migration.rs index 0b8a5f0a..3a6a4967 100644 --- a/src/server/ca/upgrades/pre_0_14_0/migration.rs +++ b/src/server/ca/upgrades/pre_0_14_0/migration.rs @@ -13,9 +13,7 @@ use crate::{ storage::KeyValueStore, }, constants::CASERVER_NS, - server::{ - config::Config, - }, + config::Config, upgrades::UpgradeResult, }; diff --git a/src/server/http/mod.rs b/src/server/http/mod.rs deleted file mode 100644 index cfb8950b..00000000 --- a/src/server/http/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ - -pub mod auth; -pub mod metrics; -pub mod request; -pub mod response; -pub mod server; -pub mod statics; -pub mod testbed; -pub mod tls; -pub mod tls_keys; - - diff --git a/src/server/http/request.rs b/src/server/http/request.rs deleted file mode 100644 index 233acc0f..00000000 --- a/src/server/http/request.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::str::FromStr; -use std::str::from_utf8; -use bytes::Bytes; -use http_body_util::{BodyExt, Limited}; -use hyper::{HeaderMap, Method}; -use hyper::body::Body; -use hyper::header::USER_AGENT; -use hyper::http::uri::PathAndQuery; -use log::info; -use rpki::ca::idexchange::MyHandle; -use serde::de::DeserializeOwned; -use crate::commons::KrillResult; -use crate::commons::actor::Actor; -use crate::commons::error::{ApiAuthError, Error}; -use crate::constants::HTTP_USER_AGENT_TRUNCATE; -use super::auth::{AuthInfo, LoggedInUser, Permission}; -use super::response::HttpResponse; -use super::server::State; - - -//------------ HyperRequest -------------------------------------------------- - -/// A type alias for the request we receive from Hyper. -pub type HyperRequest = hyper::Request; - - - -//------------ Request ------------------------------------------------------- - -pub struct Request { - request: HyperRequest, - path: RequestPath, - state: State, - auth: AuthInfo, -} - -impl Request { - pub async fn new(request: HyperRequest, state: State) -> Self { - let path = RequestPath::from_request(&request); - let auth = state.authenticate_request(&request).await; - - Request { - request, - path, - state, - auth, - } - } - - pub fn headers(&self) -> &HeaderMap { - self.request.headers() - } - - pub fn user_agent(&self) -> Option { - match self.headers().get(&USER_AGENT) { - None => None, - Some(value) => value.to_str().ok().map(|s| { - // Note: HeaderValue.to_str() only returns ok in case the - // value is plain ascii so it's safe to - // treat bytes as characters here. - if s.len() > HTTP_USER_AGENT_TRUNCATE { - s[..HTTP_USER_AGENT_TRUNCATE].to_string() - } else { - s.to_string() - } - }), - } - } - - pub async fn upgrade_from_anonymous(&mut self, auth: AuthInfo) { - if self.auth.actor().is_anonymous() { - self.auth = auth; - info!( - "Permitted anonymous actor to become actor '{}' \ - for the duration of this request", - self.auth.actor().name() - ); - } - } - - pub fn check_permission( - &self, - permission: Permission, - resource: Option<&MyHandle> - ) -> Result<(), ApiAuthError> { - self.auth.check_permission(permission, resource) - } - - pub fn actor(&self) -> Actor { - self.auth.actor().clone() - } - - pub fn auth_info(&self) -> &AuthInfo { - &self.auth - } - - pub fn auth_info_mut(&mut self) -> &mut AuthInfo { - &mut self.auth - } - - pub fn request(&self) -> &HyperRequest { - &self.request - } - - /// Returns the complete path. - pub fn path(&self) -> &RequestPath { - &self.path - } - - /// Get the application State - pub fn state(&self) -> &State { - &self.state - } - - /// Returns the method of this request. - pub fn method(&self) -> &Method { - self.request.method() - } - - /// Returns whether the request is a GET request. - pub fn is_get(&self) -> bool { - self.request.method() == Method::GET - } - - /// Returns whether the request is a GET request. - pub fn is_post(&self) -> bool { - self.request.method() == Method::POST - } - - /// Returns whether the request is a DELETE request. - pub fn is_delete(&self) -> bool { - self.request.method() == Method::DELETE - } - - /// Get a json object from a post body - pub async fn json(self) -> Result { - let bytes = self.api_bytes().await?; - - let string = - from_utf8(&bytes).map_err(|_| Error::InvalidUtf8Input)?; - serde_json::from_str(string).map_err(Error::JsonError) - } - - pub async fn api_bytes(self) -> Result { - let limit = self.state().config.post_limit_api; - self.read_bytes(limit).await - } - - pub async fn rfc6492_bytes(self) -> Result { - let limit = self.state().config.post_limit_rfc6492; - self.read_bytes(limit).await - } - - pub async fn rfc8181_bytes(self) -> Result { - let limit = self.state().config.post_limit_rfc8181; - self.read_bytes(limit).await - } - - pub async fn read_bytes(self, limit: u64) -> Result { - // We’re going to cheat a bit. If we know the body is too big from - // the Content-Length header, we return Error::PostTooBig. But if - // we don’t -- which means there are multiple chunks or somesuch -- - // we just use http_body_utils::Limited and return PostCannotRead - // on any error. - - if self.request.body().size_hint().lower() > limit { - return Err(Error::PostTooBig); - } - - Ok(Limited::new( - self.request.into_body(), - limit.try_into().unwrap_or(usize::MAX), - ) - .collect() - .await - .map_err(|_| Error::PostCannotRead)? - .to_bytes()) - } - - pub async fn get_login_url(&self) -> KrillResult { - self.state.get_login_url().await - } - - pub async fn login(&self) -> KrillResult { - self.state.login(&self.request).await - } - - pub async fn logout(&self) -> KrillResult { - self.state.logout(&self.request).await - } -} - -//------------ RequestPath --------------------------------------------------- - -#[derive(Clone, Debug)] -pub struct RequestPath { - path: PathAndQuery, - segment: (usize, usize), -} - -impl std::fmt::Display for RequestPath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.full()) - } -} - -impl RequestPath { - pub fn from_request(request: &hyper::Request) -> Self { - let path = request.uri().path_and_query().unwrap().clone(); - let mut res = RequestPath { - path, - segment: (0, 0), - }; - res.next_segment(); - res - } - - pub fn full(&self) -> &str { - self.path.path() - } - - pub fn remaining(&self) -> &str { - &self.full()[self.segment.1..] - } - - pub fn segment(&self) -> &str { - &self.full()[self.segment.0..self.segment.1] - } - - fn next_segment(&mut self) -> bool { - let mut start = self.segment.1; - let path = self.full(); - // Start beyond the length of the path signals the end. - if start >= path.len() { - return false; - } - // Skip any leading slashes. There may be multiple which should be - // folded into one (or at least that’s what we do). - while path.split_at(start).1.starts_with('/') { - start += 1 - } - // Find the next slash. If we have one, that’s the end of - // our segment, otherwise, we go all the way to the end of the path. - let end = path[start..] - .find('/') - .map(|x| x + start) - .unwrap_or_else(|| path.len()); - self.segment = (start, end); - true - } - - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option<&str> { - if self.next_segment() { - Some(self.segment()) - } else { - None - } - } - - pub fn path_arg(&mut self) -> Option - where - T: FromStr, - { - self.next().and_then(|s| T::from_str(s).ok()) - } -} diff --git a/src/server/http/server.rs b/src/server/http/server.rs deleted file mode 100644 index d45536dc..00000000 --- a/src/server/http/server.rs +++ /dev/null @@ -1,2455 +0,0 @@ -//! Hyper based HTTP server for Krill. -use std::fs::File; -use std::io::Read; -use std::net::SocketAddr; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::Arc; -use std::{env, process}; - -use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; -use base64::engine::Engine as _; -use bytes::Bytes; -use hyper::header::HeaderName; -use hyper::http::HeaderValue; -use hyper::service::service_fn; -use hyper::Method; -use hyper_util::rt::{TokioExecutor, TokioIo}; -use log::{debug, info, error, log_enabled, trace, warn}; -use rpki::ca::idexchange; -use rpki::ca::idexchange::{ - CaHandle, ChildHandle, MyHandle, ParentHandle, PublisherHandle, -}; -use rpki::repository::resources::Asn; -use serde::Serialize; -use tokio::net::TcpListener; -use tokio::select; -use tokio_rustls::TlsAcceptor; -use tokio::sync::oneshot; - -use crate::{ - commons::{ - file, - error::Error, - eventsourcing::AggregateStoreError, - }, - constants::{ - KRILL_ENV_HTTP_LOG_INFO, KRILL_ENV_UPGRADE_ONLY, ta_handle, - }, - server::{ - config::Config, - http::{ - statics::statics, testbed::testbed, tls, tls_keys, - }, - http::auth::Permission, - http::request::{HyperRequest, Request, RequestPath}, - http::response::{HttpResponse, HyperResponse}, - krillserver::KrillServer, - properties::PropertiesManager, - }, - upgrades::{ - finalise_data_migration, post_start_upgrade, - prepare_upgrade_data_migrations, UpgradeError, UpgradeMode, - }, -}; -use crate::api::admin::{ - ApiRepositoryContact, ParentCaReq, PublisherList, RepositoryContact, - Token, -}; -use crate::api::aspa::AspaDefinitionUpdates; -use crate::api::bgp::BgpAnalysisAdvice; -use crate::api::ca::RtaName; -use crate::api::history::CommandHistoryCriteria; -use crate::api::roa::RoaConfigurationUpdates; - - -//------------ 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) { - if let Err(e) = - file::save(process::id().to_string().as_bytes(), config.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("tls_keys_dir", config.tls_keys_dir()); - test_data_dir_or_die("repo_dir", config.repo_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, - mut signal_running: Option>, -) -> Result<(), Error> { - write_pid_file_or_die(&config); - test_data_dirs_or_die(&config); - - // Set up the runtime properties manager, so that we can check - // the version used for the current data in storage - let properties_manager = PropertiesManager::create( - &config.storage_uri, - config.use_history_cache, - )?; - - // Call upgrade, this will only do actual work if needed. - let upgrade_report = prepare_upgrade_data_migrations(UpgradeMode::PrepareToFinalise, &config, &properties_manager) - .map_err(|e| match e { - UpgradeError::CodeOlderThanData(_,_) => { - Error::Custom(e.to_string()) - }, - _ => Error::Custom(format!("Upgrade data migration failed with error: {}\n\nNOTE: your data was not changed. Please downgrade your krill instance to your previous version.", e)) - })?; - - if let Some(report) = &upgrade_report { - finalise_data_migration(report.versions(), &config, &properties_manager).map_err(|e| { - Error::Custom(format!( - "Finishing prepared migration failed unexpectedly. Please check your data {}. 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.storage_uri, - 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, &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.tls_keys_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(), - signal_running.take(), - )) - }), - ); - - select!( - _ = server_futures => error!("http server stopped unexpectedly"), - _ = scheduler_future => error!("scheduler stopped unexpectedly"), - ); - - Err(Error::custom("stopping krill process")) -} - -/// Runs an HTTP listener on a single socket. -async fn single_http_listener( - krill_server: Arc, - addr: SocketAddr, - config: Arc, - signal_running: Option>, -) { - let listener = match TcpListener::bind(addr).await { - Ok(listener) => listener, - Err(err) => { - error!("Could not bind to {}: {}", addr, err); - return; - } - }; - - let tls = if config.https_mode().is_disable_https() { - None - } else { - match tls::create_server_config( - &tls_keys::key_file_path(config.tls_keys_dir()), - &tls_keys::cert_file_path(config.tls_keys_dir()), - ) { - Ok(config) => Some(TlsAcceptor::from(Arc::new(config))), - Err(err) => { - error!("{}", err); - return; - } - } - }; - - if let Some(tx) = signal_running { - let _ = tx.send(()); - } - - loop { - let stream = match listener.accept().await { - Ok((stream, _addr)) => { - tls::MaybeTlsTcpStream::new(stream, tls.as_ref()) - } - Err(err) => { - error!("Fatal error in HTTP server {}: {}", addr, err); - return; - } - }; - let server = krill_server.clone(); - tokio::task::spawn(async move { - let _ = hyper_util::server::conn::auto::Builder::new( - TokioExecutor::new(), - ) - .serve_connection( - TokioIo::new(stream), - service_fn(move |req| { - let server = server.clone(); - async move { map_requests(req, server).await } - }), - ) - .await; - }); - } -} - -struct RequestLogger { - req_method: hyper::Method, - req_path: String, -} - -impl RequestLogger { - fn begin(req: &HyperRequest) -> 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() - ); - } - - if response.loggable() && log_enabled!(log::Level::Trace) { - trace!( - "{} {} {}", - self.req_method, - self.req_path, - response.status() - ); - trace!( - "Response: headers={:?} body={:?}", - response.headers(), - response.body() - ); - } - } - Err(err) => { - error!( - "{} {} Error: {}", - self.req_method, self.req_path, err - ); - } - } - } -} - -async fn map_requests( - req: HyperRequest, - state: State, -) -> Result { - let logger = RequestLogger::begin(&req); - - let mut req = Request::new(req, state).await; - - // Save any updated auth details, e.g. if an OpenID Connect token needed - // refreshing. - let new_token = req.auth_info_mut().take_new_token(); - - // 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 = super::metrics::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_token_to_response(res, new_token); - - // Log the request and the response. - logger.end(res.as_ref()); - - res.map(|res| res.into_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) -> Result { - 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_ENGINE.encode(json); - let location = format!("/ui/login?error={}", b64); - Ok(HttpResponse::found(&location)) -} - -pub fn render_empty_res(res: Result<(), Error>) -> Result { - match res { - Ok(()) => render_ok(), - Err(e) => render_error(e), - } -} - -#[allow(clippy::unnecessary_wraps)] -fn render_error(e: Error) -> Result { - debug!("Server Error: {}", e); - Ok(HttpResponse::response_from_error(e)) -} - -#[allow(clippy::unnecessary_wraps)] -fn render_json(obj: O) -> Result { - Ok(HttpResponse::json(&obj)) -} - -fn render_json_res(res: Result) -> Result { - 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() -> Result { - 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() -> Result { - Ok(HttpResponse::ok()) -} - -#[allow(clippy::unnecessary_wraps)] -pub fn render_unknown_method() -> Result { - Ok(HttpResponse::response_from_error(Error::ApiUnknownMethod)) -} - -/// A clean 404 response -#[allow(clippy::unnecessary_wraps)] -pub async fn render_not_found(_req: Request) -> Result { - Ok(HttpResponse::not_found()) -} - -/// Returns the server health. -pub async fn health(req: Request) -> Result { - if req.is_get() && req.path().segment() == "health" { - render_ok() - } else { - Err(req) - } -} - -//------------ Publication --------------------------------------------------- - -/// Handle RFC8181 queries and return the appropriate response. -pub async fn rfc8181(req: Request) -> Result { - 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) -> Result { - 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) -> Result { - 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) -> Result { - 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) -> Result { - 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) -> Result { - 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.into_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_token_to_response( - res: Result, - opt_token: Option, -) -> Result { - if let Some(token) = opt_token { - 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, Option::<&MyHandle>::None, $action, true) - }}; - ($req:ident, $perm:expr, $action:expr) => {{ - aa!($req, $perm, Option::<&MyHandle>::None, $action, false) - }}; - (no_warn $req:ident, $perm:expr, $resource:expr, $action:expr) => {{ - aa!($req, $perm, Some(&$resource), $action, true) - }}; - ($req:ident, $perm:expr, $resource:expr, $action:expr) => {{ - aa!($req, $perm, Some(&$resource), $action, false) - }}; - ($req:ident, $perm:expr, $resource:expr, $action:expr, $benign:expr) => {{ - match $req.check_permission($perm, $resource) { - Ok(()) => { $action } - Err(err) => { - Ok( - HttpResponse::forbidden( - err.to_string() - ).with_benign($benign) - ) - } - } - }}; -} - -/// Maps the API methods -async fn api(req: Request) -> Result { - 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::PubAdmin, - api_publication_server(req, &mut path).await - ), - Some("ta") => aa!( - req, - Permission::CaAdmin, - api_ta(req, &mut path).await - ), - _ => render_unknown_method(), - } - }) - } - } - } -} - -async fn api_authorized(req: Request) -> Result { - // 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) -> Result { - 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) -> Result { - match path.path_arg::() { - Some(ca) => aa!(req, Permission::CaRead, 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, -) -> Result { - 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, -) -> Result { - 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, -) -> Result { - 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, -) -> Result { - 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, -) -> Result { - 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, -) -> Result { - aa!(req, Permission::CaUpdate, 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, -) -> Result { - 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, -) -> Result { - 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>, -) -> Result { - aa!(req, Permission::PubList, { - let seconds = seconds.unwrap_or(""); - match i64::from_str(seconds) { - Ok(seconds) => { - render_json_res(req.state().repo_stats().map(|stats| { - PublisherList::from_slice(&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) -> Result { - aa!(req, Permission::PubList, { - render_json_res( - req.state() - .publishers() - .map(|publishers| PublisherList::from_slice(&publishers)), - ) - }) -} - -/// Adds a publisher -pub async fn api_add_pbl(req: Request) -> Result { - aa!(req, Permission::PubCreate, { - 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, -) -> Result { - aa!(req, Permission::PubDelete, { - 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, -) -> Result { - aa!( - req, - Permission::PubRead, - 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, -) -> Result { - aa!(req, Permission::PubRead, { - 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, -) -> Result { - aa!(req, Permission::PubRead, { - 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) -> Result { - aa!(req, Permission::CaUpdate, 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, -) -> Result { - aa!(req, Permission::CaUpdate, 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, -) -> Result { - aa!(req, Permission::CaUpdate, 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, -) -> Result { - aa!( - req, - Permission::CaRead, - ca, - render_json_res(req.state().ca_child_show(&ca, &child).await) - ) -} - -async fn api_ca_child_export( - req: Request, - ca: CaHandle, - child: ChildHandle, -) -> Result { - aa!( - req, - Permission::CaRead, - ca, - render_json_res(req.state().api_ca_child_export(&ca, &child).await) - ) -} - -async fn api_ca_child_import(req: Request, ca: CaHandle) -> Result { - aa!(req, Permission::CaAdmin, ca, { - let actor = req.actor(); - let server = req.state().clone(); - match req.json().await { - Ok(import_child) => render_empty_res( - server.api_ca_child_import(&ca, import_child, &actor).await, - ), - Err(e) => render_error(e), - } - }) -} - -async fn api_ca_stats_child_connections( - req: Request, - ca: CaHandle, -) -> Result { - aa!( - req, - Permission::CaRead, - 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, -) -> Result { - aa!( - req, - Permission::CaRead, - 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, -) -> Result { - aa!(req, Permission::CaRead, 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) -> Result { - match *req.method() { - Method::POST => aa!(req, Permission::CaAdmin, { - 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) -> Result { - match *req.method() { - Method::GET => aa!(req, Permission::CaRead, { - render_json_res( - req.state().all_ca_issues(req.auth_info()).await - ) - }), - _ => render_unknown_method(), - } -} - -/// Returns the health (state) for a given CA. -async fn api_ca_issues(req: Request, ca: CaHandle) -> Result { - match *req.method() { - Method::GET => aa!( - req, - Permission::CaRead, - ca, - render_json_res(req.state().ca_issues(&ca).await) - ), - _ => render_unknown_method(), - } -} - -async fn api_cas_list(req: Request) -> Result { - aa!(req, Permission::CaList, { - render_json_res(req.state().ca_list(req.auth_info())) - }) -} - -pub async fn api_ca_init(req: Request) -> Result { - aa!(req, Permission::CaCreate, { - 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, -) -> Result { - match *req.method() { - Method::POST => aa!(req, Permission::CaUpdate, 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) -> Result { - aa!( - req, - Permission::CaRead, - handle, - render_json_res(req.state().ca_info(&handle).await) - ) -} - -async fn api_ca_delete(req: Request, handle: CaHandle) -> Result { - let actor = req.actor(); - aa!( - req, - Permission::CaDelete, - handle, - render_json_res(req.state().ca_delete(&handle, &actor).await) - ) -} - -async fn api_ca_my_parent_contact( - req: Request, - ca: CaHandle, - parent: ParentHandle, -) -> Result { - aa!( - req, - Permission::CaRead, - ca, - render_json_res(req.state().ca_my_parent_contact(&ca, &parent).await) - ) -} - -async fn api_ca_my_parent_statuses( - req: Request, - ca: CaHandle, -) -> Result { - aa!( - req, - Permission::CaRead, - ca, - render_json_res( - req.state() - .ca_status(&ca) - .map(|s| s.parents().clone()) - ) - ) -} - -async fn api_ca_aspas( - req: Request, - path: &mut RequestPath, - ca: CaHandle, -) -> Result { - 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, -) -> Result { - // 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, -) -> Result { - aa!(req, Permission::BgpsecRead, ca, { - render_json_res(req.state().ca_bgpsec_definitions_show(ca).await) - }) -} - -async fn api_ca_bgpsec_definitions_update( - req: Request, - ca: CaHandle, -) -> Result { - aa!(req, Permission::BgpsecUpdate, 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, -) -> Result { - 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 - } - Some("export") => api_ca_child_export(req, ca, child).await, - Some("import") => api_ca_child_import(req, ca).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, -) -> Result { - match *req.method() { - Method::GET => { - aa!(req, Permission::CaRead, handle, { - // /api/v1/cas/{ca}/history/commands - // //// - let mut crit = CommandHistoryCriteria { - rows_limit: Some(path.path_arg().unwrap_or(100)), - .. Default::default() - }; - if let Some(offset) = path.path_arg() { - crit.offset = offset - } - crit.after = path.path_arg(); - crit.before = path.path_arg(); - - 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, -) -> Result { - 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, -) -> Result { - // /api/v1/cas/{ca}/command/ - match path.path_arg() { - Some(key) => match *req.method() { - Method::GET => { - aa!(req, Permission::CaRead, 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) -> Result { - match *req.method() { - Method::GET => aa!( - req, - Permission::CaRead, - 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) -> Result { - match *req.method() { - Method::GET => aa!( - req, - Permission::CaRead, - 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, -) -> Result { - match *req.method() { - Method::GET => aa!( - req, - Permission::CaRead, - 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, -) -> Result { - match *req.method() { - Method::GET => aa!( - req, - Permission::CaRead, - 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) -> Result { - aa!( - req, - Permission::CaRead, - ca, - render_json_res(req.state().ca_repo_details(&ca).await) - ) -} - -async fn api_ca_repo_status(req: Request, ca: CaHandle) -> Result { - match *req.method() { - Method::GET => aa!( - req, - Permission::CaRead, - ca, - render_json_res( - req.state() - .ca_status(&ca) - .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(" Result { - aa!(req, Permission::CaUpdate, 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, -) -> Result { - aa!(req, Permission::CaUpdate, 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(" Result { - aa!(req, Permission::CaUpdate, 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) -> Result { - aa!(req, Permission::CaUpdate, 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) -> Result { - aa!(req, Permission::CaUpdate, 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, -) -> Result { - aa!(req, Permission::AspasRead, 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, -) -> Result { - aa!(req, Permission::AspasUpdate, 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, -) -> Result { - aa!(req, Permission::AspasUpdate, 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, -) -> Result { - aa!(req, Permission::AspasUpdate, ca, { - let actor = req.actor(); - let state = req.state().clone(); - - let updates = AspaDefinitionUpdates { - add_or_replace: Vec::new(), - remove: 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) -> Result { - aa!(req, Permission::RoutesUpdate, 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, -) -> Result { - aa!(req, Permission::RoutesUpdate, ca, { - let actor = req.actor(); - let state = req.state().clone(); - - match req.json::().await { - Err(e) => render_error(e), - Ok(mut 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 - updates.set_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 { - effect, suggestion, - }) - } - } - } - } - } - } - } - }) -} - -/// show the route authorizations for this CA -async fn api_ca_routes_show(req: Request, ca: CaHandle) -> Result { - aa!(req, Permission::RoutesRead, 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, -) -> Result { - aa!(req, Permission::RoutesAnalysis, 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) -> Result { - match *req.method() { - Method::POST => aa!(req, Permission::CaAdmin, { - render_empty_res(req.state().republish_all(force).await) - }), - _ => render_unknown_method(), - } -} - -async fn api_resync_all(req: Request) -> Result { - match *req.method() { - Method::POST => aa!(req, Permission::CaAdmin, { - render_empty_res(req.state().cas_repo_sync_all(req.auth_info())) - }), - _ => render_unknown_method(), - } -} - -/// Refresh all CAs -async fn api_refresh_all(req: Request) -> Result { - match *req.method() { - Method::POST => aa!(req, Permission::CaAdmin, { - 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) -> Result { - match *req.method() { - Method::POST => aa!(req, Permission::CaAdmin, { - render_empty_res(req.state().cas_schedule_suspend_all()) - }), - _ => render_unknown_method(), - } -} - -//------------ Serve RRDP Files ---------------------------------------------- - -async fn rrdp(req: Request) -> Result { - 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, -) -> Result { - 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) -> Result { - aa!( - req, - Permission::RtaList, - ca, - render_json_res(req.state().rta_list(ca).await) - ) -} - -async fn api_ca_rta_show( - req: Request, - ca: CaHandle, - name: RtaName, -) -> Result { - aa!( - req, - Permission::RtaRead, - ca, - render_json_res(req.state().rta_show(ca, name).await) - ) -} - -async fn api_ca_rta_sign( - req: Request, - ca: CaHandle, - name: RtaName, -) -> Result { - aa!(req, Permission::RtaUpdate, 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, -) -> Result { - aa!(req, Permission::RtaUpdate, 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, -) -> Result { - aa!(req, Permission::RtaUpdate, 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) -> Result { - // - // 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/update update 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_handle(); - let server = req.state().clone(); - let actor = req.actor(); - - 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(); - 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("update") => { - let server = req.state().clone(); - let actor = req.actor(); - match req.json().await { - Ok(ta_signer_info) => render_empty_res( - server - .ta_proxy_signer_update(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(); - - 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_handle(), child) - .await, - ), - Some("parent_response.xml") => { - match req - .state() - .ca_parent_response(&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(), - } -} - - -//----------- Auth-related --------------------------------------------------- - -pub const AUTH_CALLBACK_ENDPOINT: &str = "/auth/callback"; -pub const AUTH_LOGIN_ENDPOINT: &str = "/auth/login"; -pub const AUTH_LOGOUT_ENDPOINT: &str = "/auth/logout"; - -#[cfg(feature = "multi-user")] -pub fn url_encode>(s: S) -> Result { - urlparse::quote(s, b"").map_err(|err| Error::custom(err.to_string())) -} - -#[cfg(feature = "multi-user")] -fn build_auth_redirect_location( - user: crate::server::http::auth::LoggedInUser -) -> Result { - fn b64_encode_attributes_with_mapped_error( - a: &impl serde::Serialize, - ) -> Result { - use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; - use base64::engine::Engine as _; - - Ok(BASE64_ENGINE.encode( - serde_json::to_string(a) - .map_err(|err| Error::custom(err.to_string()))?, - )) - } - - let attributes = b64_encode_attributes_with_mapped_error( - user.attributes() - )?; - - Ok(format!( - "/ui/login?token={}&id={}&attributes={}", - &url_encode(user.token())?, - &url_encode(user.id())?, - &url_encode(attributes)?, - )) -} - -pub async fn auth(req: Request) -> Result { - match req.path().full() { - #[cfg(feature = "multi-user")] - AUTH_CALLBACK_ENDPOINT if *req.method() == Method::GET => { - if log_enabled!(log::Level::Trace) { - trace!( - "Authentication callback invoked: {:?}", &req.request() - ); - } - - req.login() - .await - .and_then(|user| { - build_auth_redirect_location(user).map_err(|err| { - Error::custom(format!( - "Unable to build redirect with logged in user details: {:?}", - err - )) - }) - }) - .map(|location| HttpResponse::found(&location)) - .or_else(render_error_redirect) - } - AUTH_LOGIN_ENDPOINT if *req.method() == Method::GET => { - req.get_login_url().await.or_else(render_error) - } - AUTH_LOGIN_ENDPOINT if *req.method() == Method::POST => { - match req.login().await { - Ok(logged_in_user) => Ok(HttpResponse::json(&logged_in_user)), - Err(err) => render_error(err), - } - } - AUTH_LOGOUT_ENDPOINT if *req.method() == Method::POST => { - req.logout().await.or_else(render_error) - } - _ => Err(req), - } -} - - - - -/* XXX The server is extensively tested in the integration tests so I don’t - * think we need it to start it here. - * -//------------ 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; - - #[tokio::test] - async fn start_krill_daemon() { - let cleanup = test::start_krill_with_default_test_config( - false, false, false, false, - ) - .await; - - cleanup(); - } - - #[tokio::test] - async fn start_krill_pubd_daemon() { - let cleanup = test::start_krill_pubd(0).await; - - cleanup(); - } -} -*/ diff --git a/src/server/http/statics.rs b/src/server/http/statics.rs deleted file mode 100644 index 75f020f5..00000000 --- a/src/server/http/statics.rs +++ /dev/null @@ -1,181 +0,0 @@ -use http_body_util::{Either, Empty}; -use hyper::{Method, StatusCode}; - -use crate::server::http::request::Request; -use crate::server::http::response::HttpResponse; - -pub async fn statics(req: Request) -> Result { - let res = match *req.method() { - Method::GET => match req.path().full() { - "/" => Ok(HttpResponse::new( - hyper::Response::builder() - .status(StatusCode::FOUND) - .header("location", "/ui") - .body(Either::Left(Empty::new())) - .unwrap(), - )), - "/ui" => Ok(HttpResponse::html(INDEX)), - - "/assets/favicon-f84116cb.ico" => Ok(HttpResponse::fav(FAVICON)), - - "/assets/index-16c05fa1.js" => Ok(HttpResponse::js(JS_INDEX)), - - "/assets/en-d3d88bc8.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_ENGLISH)) - } - "/assets/de-faf2935a.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_GERMAN)) - } - "/assets/es-52cbfc21.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_SPANISH)) - } - "/assets/fr-ac1aafd8.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_FRENCH)) - } - "/assets/gr-5a66c94a.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_GREEK)) - } - "/assets/nl-f2dd1189.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_DUTCH)) - } - "/assets/pt-e9bf4047.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_PORTUGUESE)) - } - "/assets/zh-Hans-f7c709f7.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_SIMPLIFIED_CHINESE)) - } - "/assets/zh-Hant-0d86c694.js" => { - Ok(HttpResponse::js(JS_TRANSLATIONS_TRADITIONAL_CHINESE)) - } - - "/assets/index-3c0611ee.css" => Ok(HttpResponse::css(CSS)), - - "/assets/check-3e734f78.svg" => Ok(HttpResponse::svg(SVG_CHECK)), - "/assets/check-green-4525c79c.svg" => { - Ok(HttpResponse::svg(SVG_CHECK_GREEN)) - } - "/assets/clipboard-4659ffea.svg" => { - Ok(HttpResponse::svg(SVG_CLIPBOARD)) - } - "/assets/download-2dfead4c.svg" => { - Ok(HttpResponse::svg(SVG_DOWNLOAD)) - } - "/assets/edit-776bf3c3.svg" => Ok(HttpResponse::svg(SVG_EDIT)), - "/assets/error-fd1fc7e1.svg" => Ok(HttpResponse::svg(SVG_ERROR)), - "/assets/krill_logo_white-05224433.svg" => { - Ok(HttpResponse::svg(SVG_KRILL_LOGO)) - } - "/assets/logout-c725fd2c.svg" => { - Ok(HttpResponse::svg(SVG_LOGOUT)) - } - "/assets/plus-e8f1d182.svg" => Ok(HttpResponse::svg(SVG_PLUS)), - "/assets/route-left-c88b44cb.svg" => { - Ok(HttpResponse::svg(SVG_ROUTE_LEFT)) - } - "/assets/route-right-17b0c46a.svg" => { - Ok(HttpResponse::svg(SVG_ROUTE_RIGHT)) - } - "/assets/search-4a30d812.svg" => { - Ok(HttpResponse::svg(SVG_SEARCH)) - } - "/assets/trash-red-65027383.svg" => { - Ok(HttpResponse::svg(SVG_TRASH_RED)) - } - "/assets/trash-d9c6ee55.svg" => Ok(HttpResponse::svg(SVG_TRASH)), - "/assets/upload-87e6fdfd.svg" => { - Ok(HttpResponse::svg(SVG_UPLOAD)) - } - "/assets/user-5d1f1b14.svg" => Ok(HttpResponse::svg(SVG_USER)), - "/assets/welcome-9fadc7f2.svg" => { - Ok(HttpResponse::svg(SVG_WELCOME)) - } - - "/assets/Inter-italic.var-d1401419.woff2" => { - Ok(HttpResponse::woff2(FONTS_ITALIC)) - } - "/assets/Inter-roman.var-17fe38ab.woff2" => { - Ok(HttpResponse::woff2(FONTS_ROMAN)) - } - - _ => Err(req), - }, - _ => Err(req), - }; - - // Do not log static responses even at TRACE level because by definition - // static responses are often of little diagnostic value and their large - // size makes it harder to see other potentially more useful log messages. - res.map(|mut res| { - res.do_not_log(); - res - }) -} - -pub static INDEX: &[u8] = include_bytes!("../../../ui/index.html"); - -static FAVICON: &[u8] = - include_bytes!("../../../ui/assets/favicon-f84116cb.ico"); - -static JS_INDEX: &[u8] = - include_bytes!("../../../ui/assets/index-16c05fa1.js"); - -static JS_TRANSLATIONS_GERMAN: &[u8] = - include_bytes!("../../../ui/assets/de-faf2935a.js"); -static JS_TRANSLATIONS_ENGLISH: &[u8] = - include_bytes!("../../../ui/assets/en-d3d88bc8.js"); -static JS_TRANSLATIONS_SPANISH: &[u8] = - include_bytes!("../../../ui/assets/es-52cbfc21.js"); -static JS_TRANSLATIONS_FRENCH: &[u8] = - include_bytes!("../../../ui/assets/fr-ac1aafd8.js"); -static JS_TRANSLATIONS_GREEK: &[u8] = - include_bytes!("../../../ui/assets/gr-5a66c94a.js"); -static JS_TRANSLATIONS_DUTCH: &[u8] = - include_bytes!("../../../ui/assets/nl-f2dd1189.js"); -static JS_TRANSLATIONS_PORTUGUESE: &[u8] = - include_bytes!("../../../ui/assets/pt-e9bf4047.js"); -static JS_TRANSLATIONS_SIMPLIFIED_CHINESE: &[u8] = - include_bytes!("../../../ui/assets/zh-Hans-f7c709f7.js"); -static JS_TRANSLATIONS_TRADITIONAL_CHINESE: &[u8] = - include_bytes!("../../../ui/assets/zh-Hant-0d86c694.js"); - -static CSS: &[u8] = include_bytes!("../../../ui/assets/index-3c0611ee.css"); - -static SVG_CHECK: &[u8] = - include_bytes!("../../../ui/assets/check-3e734f78.svg"); -static SVG_CHECK_GREEN: &[u8] = - include_bytes!("../../../ui/assets/check-green-4525c79c.svg"); -static SVG_CLIPBOARD: &[u8] = - include_bytes!("../../../ui/assets/clipboard-4659ffea.svg"); -static SVG_DOWNLOAD: &[u8] = - include_bytes!("../../../ui/assets/download-2dfead4c.svg"); -static SVG_EDIT: &[u8] = - include_bytes!("../../../ui/assets/edit-776bf3c3.svg"); -static SVG_ERROR: &[u8] = - include_bytes!("../../../ui/assets/error-fd1fc7e1.svg"); -static SVG_KRILL_LOGO: &[u8] = - include_bytes!("../../../ui/assets/krill_logo_white-05224433.svg"); -static SVG_LOGOUT: &[u8] = - include_bytes!("../../../ui/assets/logout-c725fd2c.svg"); -static SVG_PLUS: &[u8] = - include_bytes!("../../../ui/assets/plus-e8f1d182.svg"); -static SVG_ROUTE_LEFT: &[u8] = - include_bytes!("../../../ui/assets/route-left-c88b44cb.svg"); -static SVG_ROUTE_RIGHT: &[u8] = - include_bytes!("../../../ui/assets/route-right-17b0c46a.svg"); -static SVG_SEARCH: &[u8] = - include_bytes!("../../../ui/assets/search-4a30d812.svg"); -static SVG_TRASH_RED: &[u8] = - include_bytes!("../../../ui/assets/trash-red-65027383.svg"); -static SVG_TRASH: &[u8] = - include_bytes!("../../../ui/assets/trash-d9c6ee55.svg"); -static SVG_UPLOAD: &[u8] = - include_bytes!("../../../ui/assets/upload-87e6fdfd.svg"); -static SVG_USER: &[u8] = - include_bytes!("../../../ui/assets/user-5d1f1b14.svg"); -static SVG_WELCOME: &[u8] = - include_bytes!("../../../ui/assets/welcome-9fadc7f2.svg"); - -static FONTS_ITALIC: &[u8] = - include_bytes!("../../../ui/assets/Inter-italic.var-d1401419.woff2"); -static FONTS_ROMAN: &[u8] = - include_bytes!("../../../ui/assets/Inter-roman.var-17fe38ab.woff2"); diff --git a/src/server/http/testbed.rs b/src/server/http/testbed.rs deleted file mode 100644 index c191fa45..00000000 --- a/src/server/http/testbed.rs +++ /dev/null @@ -1,142 +0,0 @@ -use hyper::Method; - -use rpki::ca::idexchange::PublisherHandle; - -use crate::{ - constants::ta_handle, - server::{ - ca::testbed_ca_handle, - http::{ - server::{ - api_add_pbl, api_ca_add_child, api_ca_child_remove, - api_ca_parent_res_xml, api_remove_pbl, - api_repository_response_xml, render_ok, - render_unknown_method, - }, - }, - http::auth::AuthInfo, - http::request::{Request, RequestPath}, - http::response::HttpResponse, - }, -}; - -//------------ Support acting as a testbed -//------------ ------------------------------------- -// -// Testbed mode enables Krill to run as an open root of a test RPKI hierarchy -// with web-UI based self-service ability for other RPKI certificate -// authorities to integrate themselves into the test RPKI hierarchy, both as -// children whose resources are delegated from the testbed and as publishers -// into the testbed repository. This feature is very similar to existing -// web-UI based self-service RPKI test hierarchies such as the RIPE NCC RPKI -// Test Environment and the APNIC RPKI Testbed. -// -// Krill can already do this via a combination of use_ta=true and the existing -// Krill API _but_ crucially the other RPKI certificate authorities would need -// to know the Krill API token in order to register themselves with the Krill -// testbed, giving them far too much power over the testbed. Testbed mode -// exposes *open* /testbed/xxx wrapper API endpoints for exchanging the RFC -// 8183 XMLs, e.g.: -// -// /testbed/enabled: should the web-UI show the testbed UI page? -// /testbed/children: in, out -// /testbed/publishers: in, out -// -// This feature assumes the existence of a built-in "testbed" CA and publisher -// when testbed mode is enabled. - -pub async fn testbed(mut req: Request) -> Result { - if !req.path().full().starts_with("/testbed") { - Err(req) // Not for us - } else if !req.state().testbed_enabled() { - render_unknown_method() - } else { - // The testbed is intended to be used without being logged in but - // anonymous users don't have the necessary rights to manipulate - // Krill CAs and publishers. Upgrade anonymous users with testbed - // rights ready for the next call in the chain to the testbed() - // API call handler functions. - req.upgrade_from_anonymous(AuthInfo::testbed()).await; - - let mut path = req.path().clone(); - match path.next() { - Some("enabled") => testbed_enabled(req).await, - Some("children") => testbed_children(req, &mut path).await, - Some("publishers") => testbed_publishers(req, &mut path).await, - _ => render_unknown_method(), - } - } -} - -// Is the testbed feature enabled or not? used by the web-UI to conditionally -// enable the testbed web-UI. -async fn testbed_enabled(req: Request) -> Result { - match *req.method() { - Method::GET => render_ok(), - _ => render_unknown_method(), - } -} - -// Open (token-less) addition/removal of child CAs under the testbed CA. -// Note: Anyone can request any resources irrespective of the resources they -// have the rights to in the real global RPKI hierarchy and anyone can -// un-register any child CA even if not "owned" by them. -async fn testbed_children( - req: Request, - path: &mut RequestPath, -) -> Result { - match (req.method().clone(), path.path_arg()) { - (Method::GET, Some(child)) => match path.next() { - Some("parent_response.xml") => { - api_ca_parent_res_xml(req, testbed_ca_handle(), child).await - } - _ => render_unknown_method(), - }, - (Method::DELETE, Some(child)) => { - api_ca_child_remove(req, testbed_ca_handle(), child).await - } - (Method::POST, None) => { - api_ca_add_child(req, testbed_ca_handle()).await - } - _ => render_unknown_method(), - } -} - -// Open (token-less) addition/removal of publishers to the testbed repository. -// Note: Anyone can become a publisher and anyone can un-register a publisher -// even if not "owned" by them. -async fn testbed_publishers( - req: Request, - path: &mut RequestPath, -) -> Result { - match (req.method().clone(), path.path_arg()) { - (Method::GET, Some(publisher)) => match path.next() { - Some("response.xml") => { - api_repository_response_xml(req, publisher).await - } - _ => render_unknown_method(), - }, - (Method::DELETE, Some(publisher)) => { - testbed_remove_pbl(req, publisher).await - } - (Method::POST, None) => api_add_pbl(req).await, - _ => render_unknown_method(), - } -} - -// Prevent deletion of the built-in TA and testbed repositories. -async fn testbed_remove_pbl( - req: Request, - publisher: PublisherHandle, -) -> Result { - if publisher.as_str() == ta_handle().as_str() - || publisher.as_str() == testbed_ca_handle().as_str() - { - Ok(HttpResponse::forbidden(format!( - "Publisher '{}' cannot be removed", - publisher - ))) - } else { - api_remove_pbl(req, publisher).await - } -} diff --git a/src/server/krillserver.rs b/src/server/manager.rs similarity index 88% rename from src/server/krillserver.rs rename to src/server/manager.rs index 5f837f5d..cf5443aa 100644 --- a/src/server/krillserver.rs +++ b/src/server/manager.rs @@ -18,7 +18,7 @@ use rpki::{ uri, }; -use crate::server::http::auth::AuthInfo; +use crate::daemon::http::auth::AuthInfo; use crate::{ commons::{ actor::Actor, @@ -27,14 +27,11 @@ use crate::{ KrillEmptyResult, KrillResult, }, constants::*, + config::Config, server::{ ca::{ - self, testbed_ca_handle, CaManager, CaStatus, + self, CaManager, CaStatus, }, - config::Config, - http::auth::{Authorizer, LoggedInUser}, - http::request::HyperRequest, - http::response::HttpResponse, mq::{now, Task, TaskQueue}, pubd::RepositoryManager, scheduler::Scheduler, @@ -44,7 +41,7 @@ use crate::api; use crate::api::admin::{ AddChildRequest, CertAuthInit, ParentCaContact, ParentCaReq, PublicationServerUris, PublisherDetails, RepoFileDeleteCriteria, - RepositoryContact, ServerInfo, UpdateChildRequest, + RepositoryContact, UpdateChildRequest, }; use crate::api::aspa::{ AspaDefinitionList, AspaDefinitionUpdates, AspaProvidersUpdate, @@ -53,10 +50,10 @@ use crate::api::aspa::{ use crate::api::bgp::{BgpAnalysisReport, BgpAnalysisSuggestion}; use crate::api::bgpsec::{BgpSecCsrInfoList, BgpSecDefinitionUpdates}; use crate::api::ca::{ - AllCertAuthIssues, CaRepoDetails, CertAuthInfo, CertAuthIssues, + CaRepoDetails, CertAuthInfo, CertAuthIssues, CertAuthList, CertAuthStats, ChildCaInfo, ChildrenConnectionStats, - IdCertInfo, ReceivedCert, RtaList, RtaName, - RtaPrepResponse, Timestamp, + IdCertInfo, RtaList, RtaName, + RtaPrepResponse, }; use crate::api::history::{ CommandDetails, CommandHistory, CommandHistoryCriteria @@ -77,17 +74,14 @@ use crate::constants::{TA_NAME, ta_handle}; use crate::server::bgp::BgpAnalyser; -//------------ KrillServer --------------------------------------------------- +//------------ KrillManager --------------------------------------------------- /// This is the Krill server that is doing all the orchestration for all /// components. -pub struct KrillServer { +pub struct KrillManager { // The base URI for this service service_uri: uri::Https, - // Component responsible for API authorization checks - authorizer: Arc, - // Publication server, with configured publishers repo_manager: Arc, @@ -100,9 +94,6 @@ pub struct KrillServer { // Shared message queue mq: Arc, - // Time this server was started - started: Timestamp, - // System actor system_actor: Actor, @@ -110,7 +101,7 @@ pub struct KrillServer { } /// # Set up and initialization -impl KrillServer { +impl KrillManager { /// Creates a new publication server. Note that state is preserved /// in the data storage. pub async fn build(config: Arc) -> KrillResult { @@ -135,9 +126,6 @@ impl KrillServer { .build()?; let signer = Arc::new(signer); - let authorizer = Arc::new(Authorizer::new(config.clone())?); - authorizer.spawn_sweep(&tokio::runtime::Handle::current()); - let system_actor = ACTOR_DEF_KRILL; // Task queue Arc is shared between ca_manager, repo_manager and the @@ -175,14 +163,12 @@ impl KrillServer { mq.schedule(Task::QueueStartTasks, now())?; - let server = KrillServer { + let server = KrillManager { service_uri, - authorizer, repo_manager, ca_manager, bgp_analyser, mq, - started: Timestamp::now(), system_actor, config: config.clone(), }; @@ -299,61 +285,40 @@ impl KrillServer { pub fn service_base_uri(&self) -> &uri::Https { &self.service_uri } - - pub fn server_info(&self) -> ServerInfo { - ServerInfo { version: crate_version!().into(), started: self.started } - } } -/// # Authentication and Access -impl KrillServer { +/// # Access to components +impl KrillManager { pub fn system_actor(&self) -> &Actor { &self.system_actor } - pub async fn authenticate_request( - &self, request: &HyperRequest - ) -> AuthInfo { - self.authorizer.authenticate_request(request).await - } - - pub async fn get_login_url(&self) -> KrillResult { - self.authorizer.get_login_url().await - } - - pub async fn login( - &self, - request: &HyperRequest, - ) -> KrillResult { - self.authorizer.login(request).await - } - - pub async fn logout( - &self, - request: &HyperRequest, - ) -> KrillResult { - self.authorizer.logout(request).await - } - pub fn testbed_enabled(&self) -> bool { self.ca_manager.testbed_enabled() } - #[cfg(feature = "multi-user")] - pub async fn login_session_cache_size(&self) -> usize { - self.authorizer.login_session_cache_size().await - } -} - -/// # Access to components -impl KrillServer { - pub fn ca_manager(&self) -> &CaManager { - &self.ca_manager + /// Converts the RRDP path portion of a HTTP request URI to a path. + /// + /// The `path` should contain everything after the `/rrdp/` portion of + /// the URI’s path. If the path is in principle valid, i.e., could + /// represent an RRDP resource generated by this RRDP sever, the method + /// will return a file system path representing this path. This does not + /// mean there will actually be a file there. The file may have been + /// deleted or may have never existed at all. This is necessary since + /// the RRDP server doesn’t track past files, only the currently valid + /// set of resources. + /// + /// If the path is definitely not valid, returns `Ok(None)`. This should + /// probably be translated into a 404 Not Found response. + pub fn resolve_rrdp_request_path( + &self, path: &str + ) -> KrillResult> { + self.repo_manager.resolve_rrdp_request_path(path) } } /// # Configure publishers -impl KrillServer { +impl KrillManager { /// Returns the repository server stats pub fn repo_stats(&self) -> KrillResult { self.repo_manager.repo_stats() @@ -408,7 +373,7 @@ impl KrillServer { } /// # Manage RFC8181 clients -impl KrillServer { +impl KrillManager { pub fn repository_response( &self, publisher: &PublisherHandle, @@ -426,26 +391,26 @@ impl KrillServer { } /// # TA Support -impl KrillServer { +impl KrillManager { pub fn ta_proxy_enabled(&self) -> bool { self.config.ta_proxy_enabled() } - pub async fn ta_proxy_init(&self) -> KrillResult<()> { + pub fn ta_proxy_init(&self) -> KrillResult<()> { self.ca_manager.ta_proxy_init() } - pub async fn ta_proxy_id(&self) -> KrillResult { + pub fn ta_proxy_id(&self) -> KrillResult { self.ca_manager.ta_proxy_id() } - pub async fn ta_proxy_publisher_request( + pub fn ta_proxy_publisher_request( &self, ) -> KrillResult { self.ca_manager.ta_proxy_publisher_request() } - pub async fn ta_proxy_repository_update( + pub fn ta_proxy_repository_update( &self, contact: RepositoryContact, actor: &Actor, @@ -454,13 +419,13 @@ impl KrillServer { .ta_proxy_repository_update(contact, actor) } - pub async fn ta_proxy_repository_contact( + pub fn ta_proxy_repository_contact( &self, ) -> KrillResult { self.ca_manager.ta_proxy_repository_contact() } - pub async fn ta_proxy_signer_add( + pub fn ta_proxy_signer_add( &self, info: TrustAnchorSignerInfo, actor: &Actor, @@ -468,7 +433,7 @@ impl KrillServer { self.ca_manager.ta_proxy_signer_add(info, actor) } - pub async fn ta_proxy_signer_update( + pub fn ta_proxy_signer_update( &self, info: TrustAnchorSignerInfo, actor: &Actor, @@ -476,20 +441,20 @@ impl KrillServer { self.ca_manager.ta_proxy_signer_update(info, actor) } - pub async fn ta_proxy_signer_make_request( + pub fn ta_proxy_signer_make_request( &self, actor: &Actor, ) -> KrillResult { self.ca_manager.ta_proxy_signer_make_request(actor) } - pub async fn ta_proxy_signer_get_request( + pub fn ta_proxy_signer_get_request( &self, ) -> KrillResult { self.ca_manager.ta_proxy_signer_get_request() } - pub async fn ta_proxy_signer_process_response( + pub fn ta_proxy_signer_process_response( &self, response: TrustAnchorSignedResponse, actor: &Actor, @@ -498,7 +463,7 @@ impl KrillServer { .ta_proxy_signer_process_response(response, actor) } - pub async fn ta_proxy_children_add( + pub fn ta_proxy_children_add( &self, child_request: AddChildRequest, actor: &Actor, @@ -512,24 +477,17 @@ impl KrillServer { ) } - pub async fn ta_cert_details(&self) -> KrillResult { + pub fn ta_cert_details(&self) -> KrillResult { let proxy = self.ca_manager.get_trust_anchor_proxy()?; Ok(proxy.get_ta_details()?.clone()) } - - pub async fn trust_anchor_cert(&self) -> Option { - self.ta_cert_details() - .await - .ok() - .map(|details| details.into()) - } } /// # Being a parent -impl KrillServer { +impl KrillManager { /// Adds a child to a CA and returns the ParentCaInfo that the child /// will need to contact this CA for resource requests. - pub async fn ca_add_child( + pub fn ca_add_child( &self, ca: &CaHandle, req: AddChildRequest, @@ -548,7 +506,7 @@ impl KrillServer { } /// Shows the parent contact for a child. - pub async fn ca_parent_response( + pub fn ca_parent_response( &self, ca: &CaHandle, child: ChildHandle, @@ -557,7 +515,7 @@ impl KrillServer { } /// Update IdCert or resources of a child. - pub async fn ca_child_update( + pub fn ca_child_update( &self, ca: &CaHandle, child: ChildHandle, @@ -568,7 +526,7 @@ impl KrillServer { } /// Update IdCert or resources of a child. - pub async fn ca_child_remove( + pub fn ca_child_remove( &self, ca: &CaHandle, child: ChildHandle, @@ -578,7 +536,7 @@ impl KrillServer { } /// Show details for a child under the CA. - pub async fn ca_child_show( + pub fn ca_child_show( &self, ca: &CaHandle, child: &ChildHandle, @@ -587,7 +545,7 @@ impl KrillServer { } /// Export a child under the CA. - pub async fn api_ca_child_export( + pub fn ca_child_export( &self, ca: &CaHandle, child: &ChildHandle, @@ -596,7 +554,7 @@ impl KrillServer { } /// Import a child under the CA. - pub async fn api_ca_child_import( + pub fn ca_child_import( &self, ca: &CaHandle, child: ImportChild, @@ -606,7 +564,7 @@ impl KrillServer { } /// Show children stats under the CA. - pub async fn ca_stats_child_connections( + pub fn ca_stats_child_connections( &self, ca: &CaHandle, ) -> KrillResult { @@ -617,9 +575,9 @@ impl KrillServer { } /// # Being a child -impl KrillServer { +impl KrillManager { /// Returns the child request for a CA, or NONE if the CA cannot be found. - pub async fn ca_child_req( + pub fn ca_child_req( &self, ca: &CaHandle, ) -> KrillResult { @@ -664,7 +622,7 @@ impl KrillServer { } /// # Stats and status of CAS -impl KrillServer { +impl KrillManager { pub async fn cas_stats( &self, ) -> KrillResult> { @@ -933,22 +891,7 @@ impl KrillServer { Ok(()) } - pub async fn all_ca_issues( - &self, - auth: &AuthInfo, - ) -> KrillResult { - let mut all_issues = AllCertAuthIssues::default(); - for ca in &self.ca_list(auth)?.cas { - let issues = self.ca_issues(&ca.handle).await?; - if !issues.is_empty() { - all_issues.cas.insert(ca.handle.clone(), issues); - } - } - - Ok(all_issues) - } - - pub async fn ca_issues( + pub fn ca_issues( &self, ca: &CaHandle, ) -> KrillResult { @@ -957,10 +900,10 @@ impl KrillServer { } /// # Synchronization operations for CAS -impl KrillServer { +impl KrillManager { /// Republish all CAs that need it. - pub async fn republish_all(&self, force: bool) -> KrillEmptyResult { - let cas = self.ca_manager.republish_all(force).await?; + pub fn republish_all(&self, force: bool) -> KrillEmptyResult { + let cas = self.ca_manager.republish_all(force)?; for ca in cas { self.cas_repo_sync_single(&ca)?; } @@ -969,8 +912,8 @@ impl KrillServer { } /// Re-sync all CAs with their repositories - pub fn cas_repo_sync_all(&self, auth: &AuthInfo) -> KrillEmptyResult { - self.ca_manager.cas_schedule_repo_sync_all(auth) + pub fn cas_repo_sync_all(&self) -> KrillEmptyResult { + self.ca_manager.cas_schedule_repo_sync_all() } /// Re-sync a specific CA with its repository @@ -979,12 +922,12 @@ impl KrillServer { } /// Refresh all CAs: ask for updates and shrink as needed. - pub async fn cas_refresh_all(&self) -> KrillEmptyResult { + pub fn cas_refresh_all(&self) -> KrillEmptyResult { self.ca_manager.cas_schedule_refresh_all() } /// Refresh a specific CA with its parents - pub async fn cas_refresh_single( + pub fn cas_refresh_single( &self, ca_handle: CaHandle, ) -> KrillEmptyResult { @@ -998,14 +941,18 @@ impl KrillServer { } /// # Admin CAS -impl KrillServer { +impl KrillManager { + pub fn ca_handles(&self) -> KrillResult> { + self.ca_manager.ca_handles().map(Vec::into_iter) + } + pub fn ca_list(&self, auth: &AuthInfo) -> KrillResult { self.ca_manager.ca_list(auth) } /// Returns the public CA info for a CA, or NONE if the CA cannot be /// found. - pub async fn ca_info(&self, ca: &CaHandle) -> KrillResult { + pub fn ca_info(&self, ca: &CaHandle) -> KrillResult { self.ca_manager.get_ca(ca).map(|ca| ca.as_ca_info()) } @@ -1029,7 +976,7 @@ impl KrillServer { /// Returns the parent contact for a CA and parent, or NONE if either the /// CA or the parent cannot be found. - pub async fn ca_my_parent_contact( + pub fn ca_my_parent_contact( &self, ca: &CaHandle, parent: &ParentHandle, @@ -1039,7 +986,7 @@ impl KrillServer { } /// Returns the history for a CA. - pub async fn ca_history( + pub fn ca_history( &self, ca: &CaHandle, crit: CommandHistoryCriteria, @@ -1057,7 +1004,7 @@ impl KrillServer { /// Returns the publisher request for a CA, or NONE of the CA cannot be /// found. - pub async fn ca_publisher_req( + pub fn ca_publisher_req( &self, ca: &CaHandle, ) -> KrillResult { @@ -1072,7 +1019,7 @@ impl KrillServer { /// Return the info about the CONFIGured repository server for a given Ca. /// and the actual objects published there, as reported by a list reply. - pub async fn ca_repo_details( + pub fn ca_repo_details( &self, ca_handle: &CaHandle, ) -> KrillResult { @@ -1094,7 +1041,7 @@ impl KrillServer { .await } - pub async fn ca_update_id( + pub fn ca_update_id( &self, ca: CaHandle, actor: &Actor, @@ -1102,7 +1049,7 @@ impl KrillServer { self.ca_manager.ca_update_id(ca, actor) } - pub async fn ca_keyroll_init( + pub fn ca_keyroll_init( &self, ca: CaHandle, actor: &Actor, @@ -1110,7 +1057,7 @@ impl KrillServer { self.ca_manager.ca_keyroll_init(ca, Duration::seconds(0), actor) } - pub async fn ca_keyroll_activate( + pub fn ca_keyroll_activate( &self, ca: CaHandle, actor: &Actor, @@ -1118,7 +1065,7 @@ impl KrillServer { self.ca_manager.ca_keyroll_activate(ca, Duration::seconds(0), actor) } - pub async fn rfc6492( + pub fn rfc6492( &self, ca: CaHandle, msg_bytes: Bytes, @@ -1130,15 +1077,15 @@ impl KrillServer { } /// # Handle ASPA requests -impl KrillServer { - pub async fn ca_aspas_definitions_show( +impl KrillManager { + pub fn ca_aspas_definitions_show( &self, - ca: CaHandle, + ca: &CaHandle, ) -> KrillResult { self.ca_manager.ca_aspas_definitions_show(ca) } - pub async fn ca_aspas_definitions_update( + pub fn ca_aspas_definitions_update( &self, ca: CaHandle, updates: AspaDefinitionUpdates, @@ -1147,7 +1094,7 @@ impl KrillServer { self.ca_manager.ca_aspas_definitions_update(ca, updates, actor) } - pub async fn ca_aspas_update_aspa( + pub fn ca_aspas_update_aspa( &self, ca: CaHandle, customer: CustomerAsn, @@ -1161,15 +1108,15 @@ impl KrillServer { } /// # Handle BGPSec requests -impl KrillServer { - pub async fn ca_bgpsec_definitions_show( +impl KrillManager { + pub fn ca_bgpsec_definitions_show( &self, - ca: CaHandle, + ca: &CaHandle, ) -> KrillResult { self.ca_manager.ca_bgpsec_definitions_show(ca) } - pub async fn ca_bgpsec_definitions_update( + pub fn ca_bgpsec_definitions_update( &self, ca: CaHandle, updates: BgpSecDefinitionUpdates, @@ -1180,8 +1127,8 @@ impl KrillServer { } /// # Handle route authorization requests -impl KrillServer { - pub async fn ca_routes_update( +impl KrillManager { + pub fn ca_routes_update( &self, ca: CaHandle, updates: RoaConfigurationUpdates, @@ -1190,7 +1137,7 @@ impl KrillServer { self.ca_manager.ca_routes_update(ca, updates, actor) } - pub async fn ca_routes_show( + pub fn ca_routes_show( &self, handle: &CaHandle, ) -> KrillResult> { @@ -1257,7 +1204,7 @@ impl KrillServer { } /// # Handle Repository Server requests -impl KrillServer { +impl KrillManager { /// Create the publication server, will fail if it was already created. pub fn repository_init( &self, @@ -1282,15 +1229,15 @@ impl KrillServer { } /// # Handle Resource Tagged Attestation requests -impl KrillServer { +impl KrillManager { /// List all known RTAs - pub async fn rta_list(&self, ca: CaHandle) -> KrillResult { + pub fn rta_list(&self, ca: CaHandle) -> KrillResult { let ca = self.ca_manager.get_ca(&ca)?; Ok(ca.rta_list()) } /// Show RTA - pub async fn rta_show( + pub fn rta_show( &self, ca: CaHandle, name: RtaName, diff --git a/src/server/mod.rs b/src/server/mod.rs index 005a859b..fb9d9dbc 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,8 +1,6 @@ pub mod bgp; pub mod ca; -pub mod config; -pub mod http; -pub mod krillserver; +pub mod manager; pub mod mq; pub mod properties; pub mod pubd; diff --git a/src/server/pubd/access.rs b/src/server/pubd/access.rs index e794c411..06e31eae 100644 --- a/src/server/pubd/access.rs +++ b/src/server/pubd/access.rs @@ -28,7 +28,7 @@ use crate::commons::eventsourcing::{ use crate::constants::{ ACTOR_DEF_KRILL, PUBSERVER_DFLT, PUBSERVER_NS, TA_NAME }; -use crate::server::config::Config; +use crate::config::Config; use super::publishers::Publisher; diff --git a/src/server/pubd/content.rs b/src/server/pubd/content.rs index ac4e0d6a..5a8b3db1 100644 --- a/src/server/pubd/content.rs +++ b/src/server/pubd/content.rs @@ -2,7 +2,7 @@ use std::fmt; use std::borrow::Cow; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use log::{debug, info}; use rpki::uri; @@ -17,7 +17,7 @@ use crate::commons::eventsourcing::{ WalChange, WalCommand, WalSet, WalStore, WalSupport, }; use crate::constants::PUBSERVER_CONTENT_NS; -use crate::server::config::{Config, RrdpUpdatesConfig}; +use crate::config::{Config, RrdpUpdatesConfig}; use super::rrdp::{ CurrentObjects, DeltaElements, RrdpServer,RrdpSession, RrdpSessionReset, RrdpUpdated, RrdpUpdateNeeded, @@ -83,6 +83,25 @@ impl RepositoryContentProxy { self.store.get_latest(&self.default_handle) } + /// Converts the RRDP path portion of a HTTP request URI to a path. + /// + /// The `path` should contain everything after the `/rrdp/` portion of + /// the URI’s path. If the path is in principle valid, i.e., could + /// represent an RRDP resource generated by this RRDP sever, the method + /// will return a file system path representing this path. This does not + /// mean there will actually be a file there. The file may have been + /// deleted or may have never existed at all. This is necessary since + /// the RRDP server doesn’t track past files, only the currently valid + /// set of resources. + /// + /// If the path is definitely not valid, returns `Ok(None)`. This should + /// probably be translated into a 404 Not Found response. + pub fn resolve_rrdp_request_path( + &self, path: &str + ) -> KrillResult> { + Ok(self.read()?.rrdp.resolve_request_path(path)) + } + /// Clears all content, so the aggregate can be re-initialized. /// /// Only to be called after all publishers have been removed from the diff --git a/src/server/pubd/manager.rs b/src/server/pubd/manager.rs index e39a8aee..ca40c841 100644 --- a/src/server/pubd/manager.rs +++ b/src/server/pubd/manager.rs @@ -1,5 +1,6 @@ //! The manager for the publication server. +use std::path::PathBuf; use std::sync::Arc; use bytes::Bytes; use log::{debug, info}; @@ -18,7 +19,7 @@ use crate::commons::actor::Actor; use crate::commons::cmslogger::CmsLogger; use crate::commons::crypto::KrillSigner; use crate::commons::error::Error; -use crate::server::config::Config; +use crate::config::Config; use crate::server::mq::{now, Task, TaskQueue}; use super::access::RepositoryAccessProxy; use super::content::RepositoryContentProxy; @@ -97,6 +98,25 @@ impl RepositoryManager { pub fn publishers(&self) -> KrillResult> { self.access.publishers() } + + /// Converts the RRDP path portion of a HTTP request URI to a path. + /// + /// The `path` should contain everything after the `/rrdp/` portion of + /// the URI’s path. If the path is in principle valid, i.e., could + /// represent an RRDP resource generated by this RRDP sever, the method + /// will return a file system path representing this path. This does not + /// mean there will actually be a file there. The file may have been + /// deleted or may have never existed at all. This is necessary since + /// the RRDP server doesn’t track past files, only the currently valid + /// set of resources. + /// + /// If the path is definitely not valid, returns `Ok(None)`. This should + /// probably be translated into a 404 Not Found response. + pub fn resolve_rrdp_request_path( + &self, path: &str + ) -> KrillResult> { + self.content.resolve_rrdp_request_path(path) + } } /// # Publication protocol support. @@ -349,7 +369,7 @@ mod tests { use crate::constants::{ ACTOR_DEF_TEST, RRDP_FIRST_SERIAL, enable_test_mode }; - use crate::server::config::{SignerConfig, SignerType}; + use crate::config::{SignerConfig, SignerType}; use crate::server::pubd::Publisher; use crate::server::pubd::rrdp::{PublicationDeltaError, RrdpServer}; use super::*; diff --git a/src/server/pubd/rrdp.rs b/src/server/pubd/rrdp.rs index 666310d6..a0a88f2e 100644 --- a/src/server/pubd/rrdp.rs +++ b/src/server/pubd/rrdp.rs @@ -27,7 +27,7 @@ use crate::constants::{ REPOSITORY_RRDP_ARCHIVE_DIR, REPOSITORY_RRDP_DIR, RRDP_FIRST_SERIAL, }; -use crate::server::config::RrdpUpdatesConfig; +use crate::config::RrdpUpdatesConfig; //------------ RRDP name definitions ----------------------------------------- @@ -169,6 +169,67 @@ impl RrdpServer { &self.snapshot } + /// Converts the RRDP path portion of a HTTP request URI to a path. + /// + /// The `path` should contain everything after the `/rrdp/` portion of + /// the URI’s path. If the path is in principle valid, i.e., could + /// represent an RRDP resource generated by this RRDP sever, the method + /// will return a file system path representing this path. This does not + /// mean there will actually be a file there. The file may have been + /// deleted or may have never existed at all. This is necessary since + /// the RRDP server doesn’t track past files, only the currently valid + /// set of resources. + /// + /// If the path is definitely not valid, returns `None`. This should + /// probably be translated into a 404 Not Found response. + pub fn resolve_request_path( + &self, path: &str + ) -> Option { + Self::is_request_path_valid(path).map(|_| { + self.rrdp_base_dir.join(path) + }) + } + + /// Check if a request path is in principle valid. + /// + /// This returns an option so we can use the question mark operator in + /// the implementation. + fn is_request_path_valid(path: &str) -> Option<()> { + // Literal "notification.xml" is fine. + if path == "notification.xml" { + return Some(()) + } + + // All other paths are session, serial, random, and then either + // "snapshot.xml" or "delta.xml" + let mut path = path.split('/'); + + // Check that the three interior items only contains letters, numbers, + // and hyphens. Technically we only do lower case letters, but since + // we are also using the Display impls of stuff, that may quietly + // change. + for item in [path.next()?, path.next()?, path.next()?] { + for &ch in item.as_bytes() { + if !ch.is_ascii_alphanumeric() && ch != b'-' { + return None + } + } + } + + // Next is the file name. + match path.next()? { + "snapshot.xml" | "delta.xml" => { } + _ => return None, + } + + // And then we need to be done. + if path.next().is_some() { + return None + } + + Some(()) + } + /// Lists all known publishers based on current objects and staged deltas. pub fn publishers(&self) -> Vec { let publisher_current_objects = @@ -208,7 +269,7 @@ impl RrdpServer { } } - /// Applies the data from an RRD session reset. + /// Applies the data from an RRDP session reset. pub fn apply_session_reset(&mut self, reset: RrdpSessionReset) { self.snapshot = reset.snapshot; self.session = reset.session; diff --git a/src/server/pubd/upgrades/mod.rs b/src/server/pubd/upgrades/mod.rs index 5021a501..53021f91 100644 --- a/src/server/pubd/upgrades/mod.rs +++ b/src/server/pubd/upgrades/mod.rs @@ -11,7 +11,7 @@ use crate::commons::KrillResult; use crate::commons::eventsourcing::WalStore; use crate::commons::storage::{Key, KeyValueStore, Segment, Scope}; use crate::constants::PUBSERVER_CONTENT_NS; -use crate::server::config::Config; +use crate::config::Config; use crate::server::pubd::content::RepositoryContent; use self::pre_0_13_0::OldRepositoryContent; diff --git a/src/server/pubd/upgrades/pre_0_10_0/migration.rs b/src/server/pubd/upgrades/pre_0_10_0/migration.rs index 00532114..62604e10 100644 --- a/src/server/pubd/upgrades/pre_0_10_0/migration.rs +++ b/src/server/pubd/upgrades/pre_0_10_0/migration.rs @@ -4,7 +4,7 @@ use crate::commons::eventsourcing::{AggregateStore, StoredCommandBuilder}; use crate::commons::storage::{KeyValueStore, Scope, Segment}; use crate::commons::version::KrillVersion; use crate::constants::PUBSERVER_NS; -use crate::server::config::Config; +use crate::config::Config; use crate::server::pubd::access::{ RepositoryAccess, RepositoryAccessEvent, RepositoryAccessInitEvent, StorableRepositoryCommand, diff --git a/src/server/scheduler.rs b/src/server/scheduler.rs index d1d7e797..e55a68ab 100644 --- a/src/server/scheduler.rs +++ b/src/server/scheduler.rs @@ -28,9 +28,9 @@ use crate::{ SCHEDULER_RESYNC_REPO_CAS_THRESHOLD, SCHEDULER_USE_JITTER_CAS_THRESHOLD, SIGNERS_NS, }, + config::Config, server::{ ca::{CaManager, CertAuth}, - config::Config, mq::{ in_hours, in_minutes, in_seconds, in_weeks, now, Task, TaskQueue, }, @@ -467,7 +467,6 @@ impl Scheduler { let cas = self .ca_manager .republish_all(false) - .await .map_err(FatalError)?; for ca_handle in cas { diff --git a/src/server/taproxy.rs b/src/server/taproxy.rs index 1060efbc..35d2c773 100644 --- a/src/server/taproxy.rs +++ b/src/server/taproxy.rs @@ -783,8 +783,8 @@ impl TrustAnchorProxy { signer: TrustAnchorSignerInfo, ) -> KrillResult> { if let Some(s) = &self.signer { - if s.ta_cert_details.cert().key_identifier() == - signer.ta_cert_details.cert().key_identifier() + if s.ta_cert_details.cert.key_identifier() == + signer.ta_cert_details.cert.key_identifier() { // It is not possible to add a signer that has a different // public key @@ -1054,7 +1054,7 @@ impl TrustAnchorProxy { let child = self.get_child_details(child_handle)?; let signing_cert = { - let received_cert = signer.ta_cert_details.cert(); + let received_cert = &signer.ta_cert_details.cert; let my_cert = received_cert.to_cert().map_err(|e| { Error::Custom(format!( "Issue with certificate held by TA: {} ", @@ -1205,7 +1205,7 @@ mod tests { storage::Namespace, test, }, - server::config::ConfigDefaults, + config::ConfigDefaults, }; use crate::tasigner::{ TrustAnchorSigner, TrustAnchorSignerInitCommand, @@ -1332,8 +1332,8 @@ mod tests { assert_eq!(ta_objects.revision().number(), 42); let ta_cert_details = proxy.get_ta_details().unwrap(); - assert_eq!(ta_cert_details.tal().uris(), &tal_https); - assert_eq!(ta_cert_details.tal().rsync_uri(), &tal_rsync); + assert_eq!(ta_cert_details.tal.uris(), &tal_https); + assert_eq!(ta_cert_details.tal.rsync_uri(), &tal_rsync); // We can make a new signer request to make a new manifest and CRL // even if we do not yet have any issued certificates to publish. diff --git a/src/tasigner/config.rs b/src/tasigner/config.rs index b5c7554a..a83fbb83 100644 --- a/src/tasigner/config.rs +++ b/src/tasigner/config.rs @@ -12,7 +12,7 @@ use url::Url; use crate::{ commons::crypto::{KrillSigner, KrillSignerBuilder, OpenSslSignerConfig}, constants::OPENSSL_ONE_OFF_SIGNER_NAME, - server::config::{LogType, SignerConfig, SignerReference, SignerType}, + config::{LogType, SignerConfig, SignerReference, SignerType}, }; // TA timing defaults @@ -92,20 +92,20 @@ impl TaTimingConfig { pub struct Config { #[serde( alias = "data_dir", - deserialize_with = "crate::server::config::deserialize_storage_uri" + deserialize_with = "crate::config::deserialize_storage_uri" )] pub storage_uri: Url, #[serde(default)] pub use_history_cache: bool, - #[serde(default = "crate::server::config::ConfigDefaults::log_type")] + #[serde(default = "crate::config::ConfigDefaults::log_type")] log_type: LogType, log_file: Option, #[serde( - default = "crate::server::config::ConfigDefaults::log_level", + default = "crate::config::ConfigDefaults::log_level", deserialize_with = "crate::commons::ext_serde::de_level_filter" )] pub log_level: LevelFilter, @@ -113,22 +113,22 @@ pub struct Config { // Signer support. Ported from main Krill. #[serde( default, - deserialize_with = "crate::server::config::deserialize_signer_ref" + deserialize_with = "crate::config::deserialize_signer_ref" )] pub default_signer: SignerReference, #[serde( default, - deserialize_with = "crate::server::config::deserialize_signer_ref" + deserialize_with = "crate::config::deserialize_signer_ref" )] pub one_off_signer: SignerReference, #[serde( - default = "crate::server::config::ConfigDefaults::signer_probe_retry_seconds" + default = "crate::config::ConfigDefaults::signer_probe_retry_seconds" )] pub signer_probe_retry_seconds: u64, - #[serde(default = "crate::server::config::ConfigDefaults::signers")] + #[serde(default = "crate::config::ConfigDefaults::signers")] pub signers: Vec, #[serde( diff --git a/src/tasigner/signer.rs b/src/tasigner/signer.rs index aa75ceca..a4436a9e 100644 --- a/src/tasigner/signer.rs +++ b/src/tasigner/signer.rs @@ -153,7 +153,7 @@ impl fmt::Display for TrustAnchorSignerEvent { write!( f, "Signer reissue done with serial {}", - ta_cert_details.cert().serial + ta_cert_details.cert.serial ) } } @@ -378,7 +378,7 @@ impl eventsourcing::Aggregate for TrustAnchorSigner { &signer, )?; let objects = TrustAnchorObjects::create( - ta_cert_details.cert(), + &ta_cert_details.cert, cmd.ta_mft_nr_override.unwrap_or(1), timing.mft_next_update_weeks, &signer, @@ -555,7 +555,7 @@ impl TrustAnchorSigner { ) .map_err(Error::custom)?; - Ok(TaCertDetails::new(rcvd_cert, tal)) + Ok(TaCertDetails { cert: rcvd_cert, tal }) } fn update_ta_cert_details( @@ -568,7 +568,7 @@ impl TrustAnchorSigner { ) -> KrillResult { let resources = ResourceSet::all(); - let key = self.ta_cert_details.cert().key_identifier(); + let key = self.ta_cert_details.cert.key_identifier(); let cert = { let serial: Serial = signer.random_serial()?; @@ -624,7 +624,7 @@ impl TrustAnchorSigner { ) .map_err(Error::custom)?; - Ok(TaCertDetails::new(rcvd_cert, tal)) + Ok(TaCertDetails { cert: rcvd_cert, tal }) } /// Process a request. @@ -646,7 +646,7 @@ impl TrustAnchorSigner { HashMap, > = HashMap::new(); - let signing_cert = self.ta_cert_details.cert(); + let signing_cert = &self.ta_cert_details.cert; let ta_rcn = ta_resource_class_name(); for child_request in &signed_request.content().child_requests { diff --git a/src/upgrades/data_migration.rs b/src/upgrades/data_migration.rs index 213e5dfe..98514921 100644 --- a/src/upgrades/data_migration.rs +++ b/src/upgrades/data_migration.rs @@ -21,9 +21,9 @@ use crate::{ KEYS_NS, PROPERTIES_NS, PUBSERVER_CONTENT_NS, PUBSERVER_NS, SIGNERS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, }, + config::Config, server::{ ca::upgrades::data_migration::check_ca_objects, - config::Config, properties::{Properties, PropertiesManager}, pubd::{RepositoryAccess, RepositoryContent}, }, diff --git a/src/upgrades/mod.rs b/src/upgrades/mod.rs index f6c83d30..3fe99b80 100644 --- a/src/upgrades/mod.rs +++ b/src/upgrades/mod.rs @@ -32,8 +32,9 @@ use crate::{ PUBSERVER_CONTENT_NS, PUBSERVER_NS, SIGNERS_NS, STATUS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, }, + config::Config, server::{ - config::Config, krillserver::KrillServer, + manager::KrillManager, properties::PropertiesManager, }, upgrades::pre_0_14_0::{ @@ -1176,7 +1177,7 @@ fn record_preexisting_openssl_keys_in_signer_mapper( /// server is started and operators can make changes. pub async fn post_start_upgrade( report: UpgradeReport, - server: &KrillServer, + server: &KrillManager, ) -> KrillResult<()> { if report.versions().from() < &KrillVersion::candidate(0, 9, 3, 2) { info!("Reissue ROAs on upgrade to force short EE certificate subjects in the objects"); @@ -1189,13 +1190,11 @@ pub async fn post_start_upgrade( add_or_replace: configs, remove: Vec::new() }; - server - .ca_aspas_definitions_update( - ca, - aspa_updates, - server.system_actor(), - ) - .await?; + server.ca_aspas_definitions_update( + ca, + aspa_updates, + server.system_actor(), + )?; } Ok(()) @@ -1296,7 +1295,7 @@ mod tests { fn test_upgrade(base_dir: &str, namespaces: &[&str]) { let temp_dir = tempdir().unwrap(); - copy_folder(&base_dir, &temp_dir); + copy_folder(base_dir, &temp_dir); // Copy data for the given names spaces into memory for testing. let mem_storage_base_uri = test::mem_storage(); @@ -1570,7 +1569,7 @@ mod tests { let source_dir_path_str = "test-resources/status_store/migration-0.9.5/"; let temp_dir = tempdir().unwrap(); - copy_folder(&source_dir_path_str, &temp_dir); + copy_folder(source_dir_path_str, &temp_dir); let source_dir_url = Url::parse( &format!("local://{}", &temp_dir.path().to_str().unwrap())) .unwrap(); diff --git a/src/upgrades/pre_0_14_0.rs b/src/upgrades/pre_0_14_0.rs index 563778b7..70782423 100644 --- a/src/upgrades/pre_0_14_0.rs +++ b/src/upgrades/pre_0_14_0.rs @@ -19,8 +19,8 @@ use crate::{ }, storage::{KeyValueStore, Namespace}, }, + config::Config, server::{ - config::Config, properties::Properties, }, server::taproxy::{ diff --git a/tests/benchmark.rs b/tests/benchmark.rs index b2728b3e..4ef8e6ca 100644 --- a/tests/benchmark.rs +++ b/tests/benchmark.rs @@ -1,7 +1,7 @@ //! Perform functional tests on a Krill instance, using the API use krill::cli::client::KrillClient; -use krill::server::config::Benchmark; +use krill::config::Benchmark; use log::LevelFilter; mod common; diff --git a/tests/common.rs b/tests/common.rs index 012b6570..67fadef9 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -23,13 +23,13 @@ use krill::commons::httpclient; use krill::commons::crypto::OpenSslSignerConfig; use krill::cli::client::KrillClient; use krill::constants::REPOSITORY_DIR; -use krill::server::config::{ +use krill::config::{ AuthType, Config, ConfigDefaults, HttpsMode, IssuanceTimingConfig, LogType, MetricsConfig, RrdpUpdatesConfig, SignerConfig, SignerReference, SignerType, TestBed, }; -use krill::server::http::tls_keys::HTTPS_SUB_DIR; -use krill::server::http::server; +use krill::daemon::http::tls_keys::HTTPS_SUB_DIR; +use krill::daemon::start::start_krill_daemon; use krill::tasigner::TaTimingConfig; @@ -392,7 +392,7 @@ impl KrillServer { let (tx, running) = oneshot::channel(); let mut res = Self { join: tokio::spawn(async { - if let Err(err) = server::start_krill_daemon( + if let Err(err) = start_krill_daemon( config.into(), Some(tx) ).await { error!("Krill failed to start: {}", err); diff --git a/tests/functional_ta.rs b/tests/functional_ta.rs index 64b1e646..f88e6ed7 100644 --- a/tests/functional_ta.rs +++ b/tests/functional_ta.rs @@ -22,7 +22,7 @@ mod common; /// /// [Krill as a Trust Anchor]: https://krill.docs.nlnetlabs.nl/en/stable/trust-anchor.html #[tokio::test] -async fn functional_at() { +async fn functional_ta() { let (mut config, _tempdir) = common::TestConfig::mem_storage() .enable_second_signer().finalize(); let port = config.port;