mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-09-22 17:34:56 +02:00
538 lines
18 KiB
Rust
538 lines
18 KiB
Rust
use std::str::{from_utf8_unchecked, FromStr};
|
|
|
|
use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
|
|
use serde::Serialize;
|
|
|
|
use rpki::x509::Time;
|
|
|
|
use crate::commons::api::{
|
|
AllCertAuthIssues, CaCommandDetails, CaCommandResult, CaRepoDetails, CertAuthInfo,
|
|
CertAuthIssues, CertAuthList, ChildCaInfo, CommandHistory, CurrentObjects, CurrentRepoState,
|
|
ParentCaContact, PublisherDetails, PublisherList, RepositoryContact, RoaDefinition, ServerInfo,
|
|
StoredEffect,
|
|
};
|
|
use crate::commons::bgp::{AnnouncementReport, BgpAnalysisReport, RoaReport};
|
|
use crate::commons::eventsourcing::WithStorableDetails;
|
|
use crate::commons::remote::api::ClientInfo;
|
|
use crate::commons::remote::rfc8183;
|
|
use crate::pubd::RepoStats;
|
|
|
|
//------------ ApiResponse ---------------------------------------------------
|
|
|
|
/// This type defines all supported responses for the api
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub enum ApiResponse {
|
|
Health,
|
|
Info(ServerInfo),
|
|
|
|
CertAuthInfo(CertAuthInfo),
|
|
CertAuthHistory(CommandHistory),
|
|
CertAuthAction(CaCommandDetails),
|
|
CertAuths(CertAuthList),
|
|
RouteAuthorizations(Vec<RoaDefinition>),
|
|
BgpAnalysisFull(BgpAnalysisReport),
|
|
BgpAnalysisAnnouncements(AnnouncementReport),
|
|
BgpAnalysisRoas(RoaReport),
|
|
|
|
ParentCaContact(ParentCaContact),
|
|
|
|
ChildInfo(ChildCaInfo),
|
|
|
|
PublisherDetails(PublisherDetails),
|
|
PublisherList(PublisherList),
|
|
RepoStats(RepoStats),
|
|
|
|
Rfc8181ClientList(Vec<ClientInfo>),
|
|
Rfc8183RepositoryResponse(rfc8183::RepositoryResponse),
|
|
Rfc8183ChildRequest(rfc8183::ChildRequest),
|
|
Rfc8183PublisherRequest(rfc8183::PublisherRequest),
|
|
|
|
RepoDetails(CaRepoDetails),
|
|
RepoState(CurrentRepoState),
|
|
|
|
CertAuthIssues(CertAuthIssues),
|
|
AllCertAuthIssues(AllCertAuthIssues),
|
|
|
|
Empty, // Typically a successful post just gets an empty 200 response
|
|
GenericBody(String), // For when the server echos Json to a successful post
|
|
}
|
|
|
|
impl ApiResponse {
|
|
pub fn report(&self, fmt: ReportFormat) -> Result<Option<String>, ReportError> {
|
|
if fmt == ReportFormat::None {
|
|
Ok(None)
|
|
} else {
|
|
match self {
|
|
ApiResponse::Health => Ok(None),
|
|
ApiResponse::Info(info) => Ok(Some(info.report(fmt)?)),
|
|
ApiResponse::CertAuths(list) => Ok(Some(list.report(fmt)?)),
|
|
ApiResponse::CertAuthInfo(info) => Ok(Some(info.report(fmt)?)),
|
|
ApiResponse::CertAuthHistory(history) => Ok(Some(history.report(fmt)?)),
|
|
ApiResponse::CertAuthAction(details) => Ok(Some(details.report(fmt)?)),
|
|
ApiResponse::CertAuthIssues(issues) => Ok(Some(issues.report(fmt)?)),
|
|
ApiResponse::AllCertAuthIssues(issues) => Ok(Some(issues.report(fmt)?)),
|
|
ApiResponse::RouteAuthorizations(auths) => Ok(Some(auths.report(fmt)?)),
|
|
ApiResponse::BgpAnalysisFull(table) => Ok(Some(table.report(fmt)?)),
|
|
ApiResponse::BgpAnalysisAnnouncements(summary) => Ok(Some(summary.report(fmt)?)),
|
|
ApiResponse::BgpAnalysisRoas(summary) => Ok(Some(summary.report(fmt)?)),
|
|
ApiResponse::ParentCaContact(contact) => Ok(Some(contact.report(fmt)?)),
|
|
ApiResponse::ChildInfo(info) => Ok(Some(info.report(fmt)?)),
|
|
ApiResponse::PublisherList(list) => Ok(Some(list.report(fmt)?)),
|
|
ApiResponse::PublisherDetails(details) => Ok(Some(details.report(fmt)?)),
|
|
ApiResponse::RepoStats(stats) => Ok(Some(stats.report(fmt)?)),
|
|
ApiResponse::Rfc8181ClientList(list) => Ok(Some(list.report(fmt)?)),
|
|
ApiResponse::Rfc8183ChildRequest(req) => Ok(Some(req.report(fmt)?)),
|
|
ApiResponse::Rfc8183PublisherRequest(req) => Ok(Some(req.report(fmt)?)),
|
|
ApiResponse::Rfc8183RepositoryResponse(res) => Ok(Some(res.report(fmt)?)),
|
|
ApiResponse::RepoDetails(details) => Ok(Some(details.report(fmt)?)),
|
|
ApiResponse::RepoState(state) => Ok(Some(state.report(fmt)?)),
|
|
ApiResponse::GenericBody(body) => Ok(Some(body.clone())),
|
|
ApiResponse::Empty => Ok(None),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------ ReportFormat --------------------------------------------------
|
|
|
|
/// This type defines the format to use when representing the api response
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ReportFormat {
|
|
None,
|
|
Json,
|
|
Text,
|
|
}
|
|
|
|
impl FromStr for ReportFormat {
|
|
type Err = ReportError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, ReportError> {
|
|
match s {
|
|
"none" => Ok(ReportFormat::None),
|
|
"json" => Ok(ReportFormat::Json),
|
|
"text" => Ok(ReportFormat::Text),
|
|
_ => Err(ReportError::UnrecognisedFormat(s.to_string())),
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------ ReportError ---------------------------------------------------
|
|
|
|
/// This type defines possible Errors for KeyStore
|
|
#[derive(Debug, Display)]
|
|
pub enum ReportError {
|
|
#[display(fmt = "This report format is not supported for this data")]
|
|
UnsupportedFormat,
|
|
|
|
#[display(fmt = "This report format is not recognised: {}", _0)]
|
|
UnrecognisedFormat(String),
|
|
}
|
|
|
|
//------------ Report --------------------------------------------------------
|
|
|
|
/// This trait should be implemented by all api responses, so that the
|
|
/// response can be formatted for users.
|
|
trait Report: Serialize {
|
|
fn text(&self) -> Result<String, ReportError>;
|
|
|
|
fn json(&self) -> String {
|
|
serde_json::to_string_pretty(self).unwrap()
|
|
}
|
|
|
|
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
|
|
match format {
|
|
ReportFormat::None => Ok("".to_string()),
|
|
ReportFormat::Json => Ok(self.json()),
|
|
ReportFormat::Text => self.text(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Report for CertAuthList {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
for ca in self.cas() {
|
|
res.push_str(&format!("{}\n", ca.handle()));
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for CertAuthInfo {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
res.push_str(&format!("Name: {}\n", self.handle()));
|
|
res.push_str("\n");
|
|
|
|
if let Some(repo_info) = self.repo_info() {
|
|
let base_uri = repo_info.base_uri();
|
|
let rrdp_uri = repo_info.rpki_notify();
|
|
res.push_str(&format!("Base uri: {}\n", base_uri));
|
|
res.push_str(&format!("RRDP uri: {}\n", rrdp_uri));
|
|
} else {
|
|
res.push_str("No repository configured.")
|
|
}
|
|
res.push_str("\n");
|
|
|
|
res.push_str(&format!("ID cert PEM:\n{}\n", self.id_cert().pem()));
|
|
res.push_str(&format!("Hash: {}\n", self.id_cert().hash()));
|
|
res.push_str("\n");
|
|
|
|
let resources = self.resources();
|
|
if resources.is_empty() {
|
|
res.push_str("Total resources: <none>\n");
|
|
} else {
|
|
res.push_str("Total resources:\n");
|
|
res.push_str(&format!(" ASNs: {}\n", resources.asn()));
|
|
res.push_str(&format!(" IPv4: {}\n", resources.v4()));
|
|
res.push_str(&format!(" IPv6: {}\n", resources.v6()));
|
|
}
|
|
res.push_str("\n");
|
|
|
|
fn print_objects(res: &mut String, objects: &CurrentObjects) {
|
|
for object in objects.names() {
|
|
res.push_str(&format!(" {}\n", object));
|
|
}
|
|
}
|
|
|
|
res.push_str("Parents:\n");
|
|
if !self.parents().is_empty() {
|
|
for parent in self.parents().iter() {
|
|
res.push_str(&format!("{}\n", parent));
|
|
}
|
|
res.push_str("\n");
|
|
} else {
|
|
res.push_str("<none>\n")
|
|
}
|
|
|
|
for (name, rc) in self.resource_classes() {
|
|
res.push_str(&format!("Resource Class: {}\n", name,));
|
|
res.push_str(&format!("Parent: {}\n", rc.parent_handle()));
|
|
res.push_str(&format!("{}", rc.keys()));
|
|
|
|
res.push_str("Current objects:\n");
|
|
print_objects(&mut res, rc.current_objects());
|
|
res.push_str("\n");
|
|
}
|
|
|
|
res.push_str("Children:\n");
|
|
if !self.children().is_empty() {
|
|
for child_handle in self.children() {
|
|
res.push_str(&format!("{}\n", child_handle));
|
|
}
|
|
} else {
|
|
res.push_str("<none>\n");
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for CommandHistory {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
res.push_str("time::command::key::success\n");
|
|
|
|
for command in self.commands() {
|
|
let success_string = match &command.effect {
|
|
StoredEffect::Error(msg) => format!("ERROR -> {}", msg),
|
|
StoredEffect::Events(_) => "OK".to_string(),
|
|
};
|
|
res.push_str(&format!(
|
|
"{}::{} ::{}::{}\n",
|
|
command.time().to_rfc3339_opts(SecondsFormat::Secs, true),
|
|
command.summary.msg,
|
|
command.key,
|
|
success_string
|
|
))
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for CaCommandDetails {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
let command = self.command();
|
|
res.push_str(&format!(
|
|
"Time: {}\n",
|
|
command.time().to_rfc3339_opts(SecondsFormat::Secs, true)
|
|
));
|
|
res.push_str(&format!("Action: {}\n", command.details().summary().msg));
|
|
|
|
match self.effect() {
|
|
CaCommandResult::Error(msg) => res.push_str(&format!("Error: {}\n", msg)),
|
|
CaCommandResult::Events(evts) => {
|
|
res.push_str("Changes:\n");
|
|
for evt in evts {
|
|
res.push_str(&format!(" {}\n", evt.details().to_string()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for ParentCaContact {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
match self {
|
|
ParentCaContact::Ta(details) => {
|
|
res.push_str(&format!("{}", details.tal()));
|
|
}
|
|
ParentCaContact::Embedded => {
|
|
res.push_str("Embedded parent");
|
|
}
|
|
ParentCaContact::Rfc6492(response) => {
|
|
let bytes = response.encode_vec();
|
|
let xml = unsafe { from_utf8_unchecked(&bytes) };
|
|
res.push_str(xml);
|
|
}
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for ChildCaInfo {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
Ok(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for PublisherList {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
res.push_str("Publishers: ");
|
|
let mut first = true;
|
|
for p in self.publishers() {
|
|
if !first {
|
|
res.push_str(", ");
|
|
} else {
|
|
first = false;
|
|
}
|
|
res.push_str(p.handle().as_str());
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for RepoStats {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
if let Some(update) = self.last_update() {
|
|
res.push_str(&format!("RRDP updated: {}\n", update.to_rfc3339()));
|
|
}
|
|
res.push_str(&format!("RRDP session: {}\n", self.session()));
|
|
res.push_str(&format!("RRDP serial: {}\n", self.serial()));
|
|
res.push_str("\n");
|
|
res.push_str("Publisher, Objects, Size, Last Updated\n");
|
|
for (publisher, stats) in self.get_publishers() {
|
|
res.push_str(&format!(
|
|
"{}, {}, {}, ",
|
|
publisher,
|
|
stats.objects(),
|
|
stats.size()
|
|
));
|
|
match stats.last_update() {
|
|
None => res.push_str("never\n"),
|
|
Some(update) => res.push_str(&format!("{}\n", update.to_rfc3339())),
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for PublisherDetails {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
res.push_str(&format!("handle: {}\n", self.handle()));
|
|
res.push_str(&format!("id: {}", self.id_cert().ski_hex()));
|
|
res.push_str(&format!("base uri: {}\n", self.base_uri().to_string()));
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for Vec<ClientInfo> {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
res.push_str("Clients: ");
|
|
for client in self.iter() {
|
|
let handle = client.handle();
|
|
let auth = client.auth();
|
|
let ski = auth.cert().ski_hex();
|
|
|
|
res.push_str(&format!(" Handle: {}, Cert (ski): {}\n", handle, ski));
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for rfc8183::RepositoryResponse {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let bytes = self.encode_vec();
|
|
let xml = unsafe { from_utf8_unchecked(&bytes) };
|
|
Ok(xml.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for rfc8183::ChildRequest {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let bytes = self.encode_vec();
|
|
let xml = unsafe { from_utf8_unchecked(&bytes) };
|
|
Ok(xml.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for rfc8183::PublisherRequest {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let bytes = self.encode_vec();
|
|
let xml = unsafe { from_utf8_unchecked(&bytes) };
|
|
Ok(xml.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for Vec<RoaDefinition> {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
for a in self.iter() {
|
|
res.push_str(&format!("{}\n", a));
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for BgpAnalysisReport {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
Ok(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for AnnouncementReport {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
Ok(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for RoaReport {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
Ok(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl Report for CaRepoDetails {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
|
|
res.push_str("Repository Details:\n");
|
|
match self.contact() {
|
|
RepositoryContact::Embedded(repo_info) => {
|
|
res.push_str(" type: embedded\n");
|
|
res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri()));
|
|
res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify()));
|
|
}
|
|
RepositoryContact::Rfc8181(response) => {
|
|
res.push_str(" type: remote\n");
|
|
res.push_str(&format!(" service uri: {}\n", response.service_uri()));
|
|
let repo_info = response.repo_info();
|
|
res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri()));
|
|
res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify()));
|
|
}
|
|
}
|
|
|
|
res.push_str("\n");
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for CurrentRepoState {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
match &self {
|
|
CurrentRepoState::Error(e) => Ok(format!("Error contacting repo! => {}\n", e)),
|
|
CurrentRepoState::List(list) => {
|
|
let mut res = String::new();
|
|
res.push_str("Available and publishing objects:\n");
|
|
let elements = list.elements();
|
|
if elements.is_empty() {
|
|
res.push_str(" <nothing>\n");
|
|
} else {
|
|
for el in elements.iter() {
|
|
res.push_str(&format!(" {} {}\n", el.hash(), el.uri()));
|
|
}
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Report for CertAuthIssues {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let mut res = String::new();
|
|
if self.is_empty() {
|
|
res.push_str("no issues found\n")
|
|
} else {
|
|
if let Some(repo_issue) = self.repo_issue() {
|
|
res.push_str(&format!("Repository Issue: {}\n", repo_issue));
|
|
}
|
|
let parent_issues = self.parent_issues();
|
|
if !parent_issues.is_empty() {
|
|
for (parent, issue) in parent_issues.iter() {
|
|
res.push_str(&format!("Parent '{}' has issue: {}\n", parent, issue));
|
|
}
|
|
}
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for AllCertAuthIssues {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let cas = self.cas();
|
|
let mut res = String::new();
|
|
if cas.is_empty() {
|
|
res.push_str("no issues found\n");
|
|
} else {
|
|
for (ca, issues) in cas.iter() {
|
|
res.push_str(&format!("Found issue for CA '{}':\n", ca));
|
|
|
|
if let Some(repo_issue) = issues.repo_issue() {
|
|
res.push_str(&format!(" Repository Issue: {}\n", repo_issue));
|
|
}
|
|
let parent_issues = issues.parent_issues();
|
|
if !parent_issues.is_empty() {
|
|
for (parent, issue) in parent_issues.iter() {
|
|
res.push_str(&format!(" Parent '{}' has issue: {}\n", parent, issue));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl Report for ServerInfo {
|
|
fn text(&self) -> Result<String, ReportError> {
|
|
let dt = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.started(), 0), Utc);
|
|
let started = Time::new(dt);
|
|
Ok(format!(
|
|
"Version: {}\nStarted: {}",
|
|
self.version(),
|
|
started.to_rfc3339()
|
|
))
|
|
}
|
|
}
|