From 35cd5d0bb22146572fe3c650c79bf01295675e4c Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Mon, 14 Apr 2025 11:17:08 +0200 Subject: [PATCH] Redesign the HTTP service. (#1264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initially, this PR was intended to more clearly separate code between Krill “business logic” – now bundled in a KrillManager –, and the HTTP server code that serves the API. The former now lives in the server module, the latter in the daemon module together with all the code to spin up a Krill daemon driving the HTTP server. However, along the way it turned into a complete redesign of how the HTTP server code works. Request handling has been split into three stages that forces implementers to check for permissions (or actively choose to not check), and read the body (or check that there isn’t one). Dispatching of the request has been restructured which should make it easier to follow what goes on where. This PR increases the minimum Rust version to 1.81. --- TODO.md | 13 +- build.rs | 175 ++ src/api/admin.rs | 2 +- src/api/pubd.rs | 17 +- src/api/status.rs | 2 +- src/api/ta.rs | 35 +- src/bin/krill.rs | 6 +- src/bin/krillup.rs | 2 +- .../crypto/signing/dispatch/krillsigner.rs | 4 +- src/commons/error.rs | 35 +- src/{server => }/config.rs | 6 +- src/constants.rs | 12 + .../http/auth/authorizer.rs | 53 +- src/{server => daemon}/http/auth/crypt.rs | 2 +- src/{server => daemon}/http/auth/mod.rs | 0 .../http/auth/permission.rs | 0 .../http/auth/providers/admin_token.rs | 17 +- .../http/auth/providers/config_file.rs | 18 +- .../http/auth/providers/mod.rs | 0 .../auth/providers/openid_connect/claims.rs | 0 .../auth/providers/openid_connect/config.rs | 0 .../providers/openid_connect/httpclient.rs | 0 .../http/auth/providers/openid_connect/mod.rs | 0 .../auth/providers/openid_connect/provider.rs | 32 +- .../auth/providers/openid_connect/util.rs | 0 src/{server => daemon}/http/auth/roles.rs | 0 src/{server => daemon}/http/auth/session.rs | 0 src/daemon/http/dispatch/api.rs | 48 + src/daemon/http/dispatch/auth.rs | 131 + src/daemon/http/dispatch/bulk.rs | 139 + src/daemon/http/dispatch/cas.rs | 1085 ++++++++ src/daemon/http/dispatch/error.rs | 38 + .../http => daemon/http/dispatch}/metrics.rs | 34 +- src/daemon/http/dispatch/mod.rs | 25 + src/daemon/http/dispatch/pubd.rs | 231 ++ src/daemon/http/dispatch/root.rs | 221 ++ src/daemon/http/dispatch/stats.rs | 63 + src/daemon/http/dispatch/ta.rs | 340 +++ src/daemon/http/dispatch/testbed.rs | 199 ++ src/daemon/http/mod.rs | 11 + src/daemon/http/request.rs | 527 ++++ src/{server => daemon}/http/response.rs | 124 +- src/daemon/http/server.rs | 185 ++ src/{server => daemon}/http/tls.rs | 0 src/{server => daemon}/http/tls_keys.rs | 0 src/daemon/http/util.rs | 14 + src/daemon/mod.rs | 8 + src/daemon/start.rs | 247 ++ src/lib.rs | 2 + src/server/ca/aspa.rs | 2 +- src/server/ca/bgpsec.rs | 2 +- src/server/ca/certauth.rs | 4 +- src/server/ca/child.rs | 2 +- src/server/ca/commands.rs | 2 +- src/server/ca/manager.rs | 38 +- src/server/ca/mod.rs | 1 - src/server/ca/publishing.rs | 2 +- src/server/ca/rc.rs | 2 +- src/server/ca/roa.rs | 2 +- src/server/ca/status.rs | 10 + src/server/ca/upgrades/data_migration.rs | 2 +- .../ca/upgrades/pre_0_10_0/migration.rs | 2 +- .../ca/upgrades/pre_0_10_0/old_events.rs | 2 +- .../ca/upgrades/pre_0_14_0/migration.rs | 4 +- src/server/http/mod.rs | 12 - src/server/http/request.rs | 267 -- src/server/http/server.rs | 2455 ----------------- src/server/http/statics.rs | 181 -- src/server/http/testbed.rs | 142 - src/server/{krillserver.rs => manager.rs} | 243 +- src/server/mod.rs | 4 +- src/server/pubd/access.rs | 2 +- src/server/pubd/content.rs | 23 +- src/server/pubd/manager.rs | 24 +- src/server/pubd/rrdp.rs | 65 +- src/server/pubd/upgrades/mod.rs | 2 +- .../pubd/upgrades/pre_0_10_0/migration.rs | 2 +- src/server/scheduler.rs | 3 +- src/server/taproxy.rs | 12 +- src/tasigner/config.rs | 16 +- src/tasigner/signer.rs | 12 +- src/upgrades/data_migration.rs | 2 +- src/upgrades/mod.rs | 21 +- src/upgrades/pre_0_14_0.rs | 2 +- tests/benchmark.rs | 2 +- tests/common.rs | 8 +- tests/functional_ta.rs | 2 +- 87 files changed, 4211 insertions(+), 3471 deletions(-) create mode 100644 build.rs rename src/{server => }/config.rs (99%) rename src/{server => daemon}/http/auth/authorizer.rs (93%) rename src/{server => daemon}/http/auth/crypt.rs (99%) rename src/{server => daemon}/http/auth/mod.rs (100%) rename src/{server => daemon}/http/auth/permission.rs (100%) rename src/{server => daemon}/http/auth/providers/admin_token.rs (88%) rename src/{server => daemon}/http/auth/providers/config_file.rs (96%) rename src/{server => daemon}/http/auth/providers/mod.rs (100%) rename src/{server => daemon}/http/auth/providers/openid_connect/claims.rs (100%) rename src/{server => daemon}/http/auth/providers/openid_connect/config.rs (100%) rename src/{server => daemon}/http/auth/providers/openid_connect/httpclient.rs (100%) rename src/{server => daemon}/http/auth/providers/openid_connect/mod.rs (100%) rename src/{server => daemon}/http/auth/providers/openid_connect/provider.rs (99%) rename src/{server => daemon}/http/auth/providers/openid_connect/util.rs (100%) rename src/{server => daemon}/http/auth/roles.rs (100%) rename src/{server => daemon}/http/auth/session.rs (100%) create mode 100644 src/daemon/http/dispatch/api.rs create mode 100644 src/daemon/http/dispatch/auth.rs create mode 100644 src/daemon/http/dispatch/bulk.rs create mode 100644 src/daemon/http/dispatch/cas.rs create mode 100644 src/daemon/http/dispatch/error.rs rename src/{server/http => daemon/http/dispatch}/metrics.rs (95%) create mode 100644 src/daemon/http/dispatch/mod.rs create mode 100644 src/daemon/http/dispatch/pubd.rs create mode 100644 src/daemon/http/dispatch/root.rs create mode 100644 src/daemon/http/dispatch/stats.rs create mode 100644 src/daemon/http/dispatch/ta.rs create mode 100644 src/daemon/http/dispatch/testbed.rs create mode 100644 src/daemon/http/mod.rs create mode 100644 src/daemon/http/request.rs rename src/{server => daemon}/http/response.rs (73%) create mode 100644 src/daemon/http/server.rs rename src/{server => daemon}/http/tls.rs (100%) rename src/{server => daemon}/http/tls_keys.rs (100%) create mode 100644 src/daemon/http/util.rs create mode 100644 src/daemon/mod.rs create mode 100644 src/daemon/start.rs delete mode 100644 src/server/http/mod.rs delete mode 100644 src/server/http/request.rs delete mode 100644 src/server/http/server.rs delete mode 100644 src/server/http/statics.rs delete mode 100644 src/server/http/testbed.rs rename src/server/{krillserver.rs => manager.rs} (88%) 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;