use std::{fmt, ops, str};
use std::collections::hash_map;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
use base64::engine::Engine as _;
use bytes::Bytes;
use chrono::{Duration, TimeZone, Utc};
use rpki::uri;
use rpki::ca::idcert::IdCert;
use rpki::ca::idexchange::{
CaHandle, ChildHandle, ParentHandle, RepoInfo, ServiceUri,
};
use rpki::ca::provisioning::{
IssuanceRequest, IssuedCert, RequestResourceLimit,
ResourceClassEntitlements, ResourceClassListResponse,
ResourceClassName,
};
use rpki::ca::publication::{Base64, PublishDelta, PublishDeltaElement};
use rpki::crypto::{KeyIdentifier, PublicKey};
use rpki::repository::aspa::Aspa;
use rpki::repository::cert::Cert;
use rpki::repository::crl::{Crl, CrlEntry};
use rpki::repository::manifest::Manifest;
use rpki::repository::resources::{Asn, ResourceSet};
use rpki::repository::roa::Roa;
use rpki::repository::x509::{Name, Serial, Time, Validity};
use rpki::rrdp::Hash;
use serde::{Deserialize, Serialize};
use crate::commons::crypto::CsrInfo;
use crate::commons::error;
use crate::commons::version::KrillVersion;
use super::admin::{PublishedFile, RepositoryContact};
use super::aspa::AspaDefinition;
use super::bgpsec::BgpSecAsnKey;
use super::status::ErrorResponse;
use super::roa::{RoaPayload, RoaPayloadJsonMapKey};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IdCertInfo {
pub public_key: PublicKey,
pub base64: Base64,
pub hash: Hash,
}
impl IdCertInfo {
pub fn pem(&self) -> IdCertPem<'_> {
IdCertPem { base64: &self.base64 }
}
}
impl From<&IdCert> for IdCertInfo {
fn from(cer: &IdCert) -> Self {
let bytes = cer.to_bytes();
IdCertInfo {
public_key: cer.public_key().clone(),
base64: Base64::from_content(&bytes),
hash: Hash::from_data(&bytes),
}
}
}
impl From<IdCert> for IdCertInfo {
fn from(cer: IdCert) -> Self {
Self::from(&cer)
}
}
impl TryFrom<&IdCertInfo> for IdCert {
type Error = error::Error;
fn try_from(info: &IdCertInfo) -> Result<Self, Self::Error> {
IdCert::decode(info.base64.to_bytes().as_ref()).map_err(|e| {
error::Error::Custom(format!(
"Could not decode IdCertInfo into IdCert: {e}"
))
})
}
}
impl fmt::Display for IdCertInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.pem())
}
}
pub struct IdCertPem<'a> {
base64: &'a Base64,
}
impl fmt::Display for IdCertPem<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("-----BEGIN CERTIFICATE-----\n")?;
for line in self
.base64
.as_str()
.as_bytes() .chunks(64)
.map(|b| unsafe { std::str::from_utf8_unchecked(b) })
{
f.write_str(line)?;
f.write_str("\n")?;
}
f.write_str("-----END CERTIFICATE-----\n")
}
}
#[derive(
Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum ChildState {
#[default]
Active,
Suspended,
}
impl ChildState {
pub fn is_suspended(self) -> bool {
matches!(self, Self::Suspended)
}
}
impl fmt::Display for ChildState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(
match &self {
ChildState::Active => "active",
ChildState::Suspended => "suspended",
}
)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ChildCaInfo {
pub state: ChildState,
pub id_cert: IdCertInfo,
pub entitled_resources: ResourceSet,
}
impl fmt::Display for ChildCaInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", self.id_cert.pem())?;
writeln!(
f,
"SHA256 hash of PEM encoded certificate: {}",
self.id_cert.hash
)?;
writeln!(f, "resources: {}", self.entitled_resources)?;
writeln!(f, "state: {}", self.state)
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Received;
pub type ReceivedCert = CertInfo<Received>;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Issued;
pub type IssuedCertificate = CertInfo<Issued>;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Suspended;
pub type SuspendedCert = CertInfo<Suspended>;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Unsuspended;
pub type UnsuspendedCert = CertInfo<Unsuspended>;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertInfo<T> {
pub uri: uri::Rsync,
pub name: ObjectName,
pub resources: ResourceSet,
pub limit: RequestResourceLimit,
pub subject: Name,
pub validity: Validity,
pub serial: Serial,
#[serde(flatten)]
pub csr_info: CsrInfo,
pub base64: Base64,
pub hash: Hash,
marker: std::marker::PhantomData<T>,
}
impl<T> CertInfo<T> {
pub fn create(
cert: Cert,
uri: uri::Rsync,
resources: ResourceSet,
limit: RequestResourceLimit,
) -> Result<Self, InvalidCert> {
let name = {
let path = uri.path();
let after_last_slash = path.rfind('/').unwrap_or(0) + 1;
if !path.ends_with(".cer") || path.len() < after_last_slash + 5 {
Err(InvalidCert::Uri(uri.clone()))
} else {
Ok(ObjectName(path[after_last_slash..].into()))
}
}?;
let key = cert.subject_public_key_info().clone();
let ca_repository = cert
.ca_repository()
.ok_or(InvalidCert::CaRepositoryMissing)?
.clone();
let rpki_manifest = cert
.rpki_manifest()
.ok_or(InvalidCert::RpkiManifestMissing)?
.clone();
let rpki_notify = cert.rpki_notify().cloned();
let csr_info =
CsrInfo::new(ca_repository, rpki_manifest, rpki_notify, key);
let subject = cert.subject().clone();
let validity = cert.validity();
let serial = cert.serial_number();
let base64 = Base64::from(&cert);
let hash = base64.to_hash();
base64.to_hash();
Ok(CertInfo {
uri,
name,
resources,
limit,
subject,
validity,
serial,
csr_info,
base64,
hash,
marker: std::marker::PhantomData,
})
}
pub fn key_identifier(&self) -> KeyIdentifier {
self.csr_info.key_id()
}
pub fn expires(&self) -> Time {
self.validity.not_after()
}
pub fn to_cert(&self) -> Result<Cert, CertInfoDecodeError> {
Cert::decode(
self.to_bytes().as_ref()
).map_err(|e| CertInfoDecodeError(e.to_string()))
}
pub fn to_rfc6492_issued_cert(
&self
) -> Result<IssuedCert, CertInfoDecodeError> {
let cert = self.to_cert()?;
Ok(IssuedCert::new(self.uri.clone(), self.limit.clone(), cert))
}
pub fn to_bytes(&self) -> Bytes {
self.base64.to_bytes()
}
pub fn to_converted<Y>(&self) -> CertInfo<Y> {
CertInfo {
uri: self.uri.clone(),
name: self.name.clone(),
resources: self.resources.clone(),
limit: self.limit.clone(),
subject: self.subject.clone(),
validity: self.validity,
serial: self.serial,
csr_info: self.csr_info.clone(),
base64: self.base64.clone(),
hash: self.hash,
marker: std::marker::PhantomData,
}
}
pub fn into_converted<Y>(self) -> CertInfo<Y> {
CertInfo {
uri: self.uri,
name: self.name,
resources: self.resources,
limit: self.limit,
subject: self.subject,
validity: self.validity,
serial: self.serial,
csr_info: self.csr_info,
base64: self.base64,
hash: self.hash,
marker: std::marker::PhantomData,
}
}
pub fn reduced_applicable_resources(
&self, encompassing: &ResourceSet,
) -> Option<ResourceSet> {
if encompassing.contains(&self.resources) {
None
} else {
Some(encompassing.intersection(&self.resources))
}
}
pub fn crl_name(&self) -> ObjectName {
ObjectName::from_key(&self.key_identifier(), "crl")
}
pub fn crl_uri(&self) -> uri::Rsync {
self.uri_for_object(self.crl_name())
}
pub fn mft_name(&self) -> ObjectName {
ObjectName::from_key(&self.key_identifier(), "mft")
}
pub fn mft_uri(&self) -> uri::Rsync {
self.uri_for_object(self.mft_name())
}
pub fn ca_repository(&self) -> &uri::Rsync {
self.csr_info.ca_repository()
}
pub fn uri_for_object(&self, name: impl Into<ObjectName>) -> uri::Rsync {
self.uri_for_name(&name.into())
}
pub fn uri_for_name(&self, name: &ObjectName) -> uri::Rsync {
self.ca_repository().join(name.as_ref()).unwrap()
}
pub fn revocation(&self) -> Revocation {
Revocation::new(self.serial, self.validity.not_after())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PendingKeyInfo {
pub key_id: KeyIdentifier,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertifiedKeyInfo {
pub key_id: KeyIdentifier,
pub incoming_cert: ReceivedCert,
pub request: Option<IssuanceRequest>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ObjectName(Arc<str>);
impl ObjectName {
pub fn new(name: impl Into<Arc<str>>) -> Self {
Self(name.into())
}
pub fn from_key(ki: &KeyIdentifier, extension: &str) -> Self {
ObjectName(format!("{ki}.{extension}").into())
}
pub fn cer_from_key(ki: &KeyIdentifier) -> Self {
ObjectName::from_key(ki, "cer")
}
pub fn mft_from_ca_key(ki: &KeyIdentifier) -> Self {
ObjectName::from_key(ki, "mft")
}
pub fn crl_from_ca_key(ki: &KeyIdentifier) -> Self {
ObjectName::from_key(ki, "crl")
}
pub fn aspa_from_customer(customer: Asn) -> Self {
ObjectName(format!("{customer}.asa").into())
}
pub fn bgpsec(asn: Asn, key: KeyIdentifier) -> Self {
ObjectName(
format!("ROUTER-{:08X}-{}.cer", asn.into_u32(), key).into(),
)
}
}
impl From<&Cert> for ObjectName {
fn from(c: &Cert) -> Self {
Self::cer_from_key(&c.subject_key_identifier())
}
}
impl From<&Manifest> for ObjectName {
fn from(m: &Manifest) -> Self {
Self::mft_from_ca_key(&m.cert().authority_key_identifier().unwrap())
}
}
impl From<&Crl> for ObjectName {
fn from(c: &Crl) -> Self {
Self::crl_from_ca_key(c.authority_key_identifier())
}
}
impl From<RoaPayloadJsonMapKey> for ObjectName {
fn from(auth: RoaPayloadJsonMapKey) -> Self {
ObjectName(format!("{}.roa", hex::encode(auth.to_string())).into())
}
}
impl From<RoaPayload> for ObjectName {
fn from(def: RoaPayload) -> Self {
ObjectName(format!("{}.roa", hex::encode(def.to_string())).into())
}
}
impl From<&AspaDefinition> for ObjectName {
fn from(aspa: &AspaDefinition) -> Self {
Self::aspa_from_customer(aspa.customer)
}
}
impl From<&BgpSecAsnKey> for ObjectName {
fn from(asn_key: &BgpSecAsnKey) -> Self {
Self::bgpsec(asn_key.asn, asn_key.key)
}
}
impl From<&str> for ObjectName {
fn from(s: &str) -> Self {
ObjectName(s.into())
}
}
impl AsRef<str> for ObjectName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<[u8]> for ObjectName {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
impl fmt::Display for ObjectName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Revocation {
serial: Serial,
#[serde(default = "Time::now")]
revocation_date: Time,
expires: Time,
}
impl Revocation {
pub fn new(serial: Serial, expires: Time) -> Self {
Revocation {
serial,
revocation_date: Time::now(),
expires,
}
}
}
impl From<&Cert> for Revocation {
fn from(cer: &Cert) -> Self {
Revocation::new(cer.serial_number(), cer.validity().not_after())
}
}
impl From<&Manifest> for Revocation {
fn from(m: &Manifest) -> Self {
Self::from(m.cert())
}
}
impl From<&Roa> for Revocation {
fn from(r: &Roa) -> Self {
Self::from(r.cert())
}
}
impl From<&Aspa> for Revocation {
fn from(aspa: &Aspa) -> Self {
Self::from(aspa.cert())
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct Revocations(Vec<Revocation>);
impl Revocations {
pub fn to_crl_entries(&self) -> Vec<CrlEntry> {
self.0.iter().map(|r| {
CrlEntry::new(r.serial, r.revocation_date)
}).collect()
}
pub fn remove_expired(&mut self) -> Vec<Revocation> {
let (relevant, expired) = self.0.iter().partition(|r| {
r.expires > Time::now()
});
self.0 = relevant;
expired
}
pub fn add(&mut self, revocation: Revocation) {
self.0.push(revocation);
}
pub fn remove(&mut self, revocation: &Revocation) {
self.0.retain(|existing| existing != revocation);
}
pub fn apply_delta(&mut self, delta: RevocationsDelta) {
self.0.retain(|r| !delta.dropped.contains(r));
for r in delta.added {
self.add(r);
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct RevocationsDelta {
added: Vec<Revocation>,
dropped: Vec<Revocation>,
}
impl RevocationsDelta {
pub fn add(&mut self, revocation: Revocation) {
self.added.push(revocation);
}
pub fn drop(&mut self, revocation: Revocation) {
self.dropped.push(revocation);
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ResourceSetSummary {
pub asn_blocks: usize,
pub ipv4_blocks: usize,
pub ipv6_blocks: usize,
}
impl From<&ResourceSet> for ResourceSetSummary {
fn from(rs: &ResourceSet) -> Self {
ResourceSetSummary {
asn_blocks: rs.asn().iter().count(),
ipv4_blocks: rs.ipv4().iter().count(),
ipv6_blocks: rs.ipv6().iter().count(),
}
}
}
impl fmt::Display for ResourceSetSummary {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"asn: {} blocks, v4: {} blocks, v6: {} blocks",
self.asn_blocks, self.ipv4_blocks, self.ipv6_blocks
)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertAuthList {
pub cas: Vec<CertAuthSummary>,
}
impl fmt::Display for CertAuthList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for ca in &self.cas {
writeln!(f, "{}", ca.handle)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertAuthSummary {
pub handle: CaHandle,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ParentKindInfo {
Ta,
Embedded,
Rfc6492,
}
impl fmt::Display for ParentKindInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParentKindInfo::Ta => write!(f, "This CA is a TA"),
ParentKindInfo::Embedded => write!(f, "Embedded parent"),
ParentKindInfo::Rfc6492 => write!(f, "RFC 6492 Parent"),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ParentInfo {
pub handle: ParentHandle,
pub kind: ParentKindInfo,
}
impl fmt::Display for ParentInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Handle: {} Kind: {}", self.handle, self.kind)
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ParentStatuses(HashMap<ParentHandle, ParentStatus>);
impl ParentStatuses {
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get(&self, parent: &ParentHandle) -> Option<&ParentStatus> {
self.0.get(parent)
}
pub fn get_or_default_mut(
&mut self,
parent: &ParentHandle,
) -> &mut ParentStatus {
if !self.0.contains_key(parent) {
self.0.insert(parent.clone(), ParentStatus::default());
}
self.0.get_mut(parent).unwrap()
}
pub fn remove(&mut self, parent: &ParentHandle) -> Option<ParentStatus> {
self.0.remove(parent)
}
pub fn insert(&mut self, parent: ParentHandle, status: ParentStatus) {
self.0.insert(parent, status);
}
pub fn iter(&self) -> hash_map::Iter<'_, ParentHandle, ParentStatus> {
self.0.iter()
}
pub fn sync_candidates(
&self,
ca_parents: Vec<&ParentHandle>,
batch: usize,
) -> Vec<ParentHandle> {
let mut parents = vec![];
for parent in ca_parents {
if !self.0.contains_key(parent) {
parents.push(parent.clone());
}
}
let mut parents_by_last_exchange = self.sorted_by_last_exchange();
parents.append(&mut parents_by_last_exchange);
parents.truncate(batch);
parents
}
fn sorted_by_last_exchange(&self) -> Vec<ParentHandle> {
let mut sorted_parents: Vec<(&ParentHandle, &ParentStatus)> =
self.iter().collect();
sorted_parents.sort_by(|a, b| {
let a_last_exchange = a.1.last_exchange.as_ref();
let b_last_exchange = b.1.last_exchange.as_ref();
let a_last_exchange_time =
a_last_exchange.map(|e| i64::from(e.timestamp)).unwrap_or(0)
/ 60;
let b_last_exchange_time =
b_last_exchange.map(|e| i64::from(e.timestamp)).unwrap_or(0)
/ 60;
if a_last_exchange_time == b_last_exchange_time {
let a_last_exchange_res = a_last_exchange
.map(|e| e.result.was_success())
.unwrap_or(false);
let b_last_exchange_res = b_last_exchange
.map(|e| e.result.was_success())
.unwrap_or(false);
a_last_exchange_res.cmp(&b_last_exchange_res)
} else {
a_last_exchange_time.cmp(&b_last_exchange_time)
}
});
sorted_parents
.into_iter()
.map(|(handle, _)| handle)
.cloned()
.collect()
}
}
impl IntoIterator for ParentStatuses {
type Item = (ParentHandle, ParentStatus);
type IntoIter = hash_map::IntoIter<ParentHandle, ParentStatus>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a ParentStatuses {
type Item = (&'a ParentHandle, &'a ParentStatus);
type IntoIter = hash_map::Iter<'a, ParentHandle, ParentStatus>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl fmt::Display for ParentStatuses {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (parent, status) in self.0.iter() {
writeln!(f, "Parent: {parent}")?;
match &status.last_exchange {
None => writeln!(f, "Status: connection still pending")?,
Some(exchange) => {
writeln!(f, "URI: {}", exchange.uri)?;
writeln!(f, "Status: {}", exchange.result)?;
writeln!(
f,
"Last contacted: {}",
exchange.timestamp.into_rfc3339()
)?;
if exchange.result.was_success() {
write!(f, "Resource Entitlements:")?;
} else {
write!(f, "LAST KNOWN Resource Entitlements:")?;
}
if status.classes.is_empty() {
writeln!(f, " None")?;
} else {
writeln!(f, " {}", status.all_resources)?;
for class in &status.classes {
writeln!(
f,
" resource class: {}",
class.class_name()
)?;
writeln!(
f,
" entitled resources: {}",
class.resource_set()
)?;
writeln!(
f,
" entitled not after: {}",
class.not_after().to_rfc3339()
)?;
let uri = class.signing_cert().url();
let cert = BASE64_ENGINE.encode(
class.signing_cert().cert()
.to_captured().as_slice()
);
writeln!(f, " issuing cert uri: {uri}")?;
writeln!(
f,
" issuing cert PEM:\n\n\
-----BEGIN CERTIFICATE-----\n\
{cert}\n\
-----END CERTIFICATE-----\n\n",
)?;
writeln!(f, " received certificate(s):")?;
for issued in class.issued_certs().iter() {
let uri = issued.uri();
let cert = BASE64_ENGINE.encode(
issued.cert().to_captured().as_slice()
);
writeln!(f, " published at: {uri}")?;
writeln!(
f,
" cert PEM:\n\n\
-----BEGIN CERTIFICATE-----\n\
{cert}\n\
-----END CERTIFICATE-----\n\n"
)?;
}
}
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ParentStatus {
pub last_exchange: Option<ParentExchange>,
pub last_success: Option<Timestamp>,
pub all_resources: ResourceSet,
#[serde(default)]
pub classes: Vec<ResourceClassEntitlements>,
}
impl ParentStatus {
pub fn opt_failure(&self) -> Option<ErrorResponse> {
self.last_exchange.as_ref().and_then(|e| e.opt_failure())
}
pub fn set_failure(&mut self, uri: ServiceUri, error: ErrorResponse) {
self.last_exchange = Some(ParentExchange {
timestamp: Timestamp::now(),
uri,
result: ExchangeResult::Failure(error),
});
}
pub fn set_entitlements(
&mut self,
uri: ServiceUri,
entitlements: &ResourceClassListResponse,
) {
self.set_last_updated(uri);
self.classes.clone_from(entitlements.classes());
let mut all_resources = ResourceSet::default();
for class in &self.classes {
all_resources = all_resources.union(class.resource_set())
}
self.all_resources = all_resources;
}
pub fn set_last_updated(&mut self, uri: ServiceUri) {
let timestamp = Timestamp::now();
self.last_exchange = Some(ParentExchange {
timestamp,
uri,
result: ExchangeResult::Success,
});
self.last_success = Some(timestamp);
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct RepoStatus {
pub last_exchange: Option<ParentExchange>,
pub last_success: Option<Timestamp>,
pub published: Vec<PublishedFile>,
}
impl RepoStatus {
pub fn opt_failure(&self) -> Option<ErrorResponse> {
self.last_exchange.as_ref().and_then(|e| e.opt_failure())
}
pub fn set_failure(&mut self, uri: ServiceUri, error: ErrorResponse) {
let timestamp = Timestamp::now();
self.last_exchange = Some(ParentExchange {
timestamp,
uri,
result: ExchangeResult::Failure(error),
});
}
pub fn update_published(&mut self, uri: ServiceUri, delta: PublishDelta) {
let timestamp = Timestamp::now();
self.last_exchange = Some(ParentExchange {
timestamp,
uri,
result: ExchangeResult::Success,
});
for element in delta.into_elements() {
match element {
PublishDeltaElement::Publish(publish) => {
let (_tag, uri, base64) = publish.unpack();
self.published.push(PublishedFile { uri, base64 });
}
PublishDeltaElement::Update(update) => {
let (_tag, uri, base64, _hash) = update.unpack();
self.published.retain(|el| el.uri != uri);
self.published.push(PublishedFile { uri, base64 });
}
PublishDeltaElement::Withdraw(withdraw) => {
let (_tag, uri, _hash) = withdraw.unpack();
self.published.retain(|el| el.uri != uri);
}
}
}
self.last_success = Some(timestamp);
}
pub fn set_last_updated(&mut self, uri: ServiceUri) {
let timestamp = Timestamp::now();
self.last_exchange = Some(ParentExchange {
timestamp,
uri,
result: ExchangeResult::Success,
});
self.last_success = Some(timestamp);
}
}
impl fmt::Display for RepoStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.last_exchange {
None => writeln!(f, "Status: connection still pending")?,
Some(exchange) => {
Time::now();
writeln!(f, "URI: {}", exchange.uri)?;
writeln!(f, "Status: {}", exchange.result)?;
writeln!(
f,
"Last contacted: {}",
exchange.timestamp.into_rfc3339()
)?;
if let Some(success) = self.last_success.as_ref() {
writeln!(
f,
"Last successful contact: {}",
success.into_rfc3339()
)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ParentExchange {
pub timestamp: Timestamp,
pub uri: ServiceUri,
pub result: ExchangeResult,
}
impl ParentExchange {
pub fn opt_failure(&self) -> Option<ErrorResponse> {
match &self.result {
ExchangeResult::Success => None,
ExchangeResult::Failure(error) => Some(error.clone()),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[allow(clippy::large_enum_variant)]
pub enum ExchangeResult {
Success,
Failure(ErrorResponse),
}
impl ExchangeResult {
pub fn was_success(&self) -> bool {
match self {
ExchangeResult::Success => true,
ExchangeResult::Failure(_) => false,
}
}
}
impl fmt::Display for ExchangeResult {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ExchangeResult::Success => write!(f, "success"),
ExchangeResult::Failure(e) => write!(f, "failure: {}", e.msg),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ChildrenConnectionStats {
pub children: Vec<ChildConnectionStats>,
}
impl ChildrenConnectionStats {
pub fn suspension_candidates(
&self,
threshold_seconds: i64,
) -> Vec<ChildHandle> {
self.children
.iter()
.filter(|child| child.is_suspension_candidate(threshold_seconds))
.map(|child| child.handle.clone())
.collect()
}
}
impl fmt::Display for ChildrenConnectionStats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !self.children.is_empty() {
writeln!(f, "handle,user_agent,last_exchange,result,state")?;
for child in &self.children {
match &child.last_exchange {
None => {
writeln!(
f,
"{},n/a,never,n/a,{}",
child.handle, child.state
)?;
}
Some(exchange) => {
let agent =
exchange.user_agent.as_deref().unwrap_or("");
writeln!(
f,
"{},{},{},{},{}",
child.handle,
agent,
exchange.timestamp.into_rfc3339(),
exchange.result,
child.state
)?;
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ChildConnectionStats {
pub handle: ChildHandle,
pub last_exchange: Option<ChildExchange>,
pub state: ChildState,
}
impl ChildConnectionStats {
pub fn is_suspension_candidate(&self, threshold_seconds: i64) -> bool {
if self.state == ChildState::Suspended {
false
}
else {
self.last_exchange.as_ref().map(|exchange| {
exchange.is_krill_above_0_9_1()
&& exchange.more_than_seconds_ago(threshold_seconds)
}).unwrap_or(false)
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ChildStatus {
pub last_exchange: Option<ChildExchange>,
pub last_success: Option<Timestamp>,
pub suspended: Option<Timestamp>,
}
impl ChildStatus {
pub fn set_success(&mut self, user_agent: Option<String>) {
let timestamp = Timestamp::now();
self.last_exchange = Some(ChildExchange {
result: ExchangeResult::Success,
timestamp,
user_agent,
});
self.last_success = Some(timestamp);
self.suspended = None;
}
pub fn set_failure(
&mut self,
user_agent: Option<String>,
error_response: ErrorResponse,
) {
self.last_exchange = Some(ChildExchange {
timestamp: Timestamp::now(),
result: ExchangeResult::Failure(error_response),
user_agent,
});
self.suspended = None;
}
pub fn set_suspended(&mut self) {
self.suspended = Some(Timestamp::now())
}
pub fn child_state(&self) -> ChildState {
if self.suspended.is_none() {
ChildState::Active
} else {
ChildState::Suspended
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ChildExchange {
pub timestamp: Timestamp,
pub result: ExchangeResult,
pub user_agent: Option<String>,
}
impl ChildExchange {
pub fn more_than_seconds_ago(&self, seconds: i64) -> bool {
self.timestamp < Timestamp::now_minus_seconds(seconds)
}
pub fn is_krill_above_0_9_1(&self) -> bool {
if let Some(agent) = &self.user_agent {
if agent == "local-child" {
return true;
}
else if
let Some(version) = agent.strip_prefix("krill/")
&& let Ok(krill_version) = KrillVersion::from_str(version)
{
return krill_version > KrillVersion::release(0, 9, 1);
}
}
false
}
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd,
Serialize,
)]
pub struct Timestamp(i64);
impl Timestamp {
pub fn new(ts: i64) -> Self {
Timestamp(ts)
}
pub fn now() -> Self {
Timestamp(Time::now().timestamp())
}
pub fn now_plus_hours(hours: i64) -> Self {
Timestamp::now().plus_hours(hours)
}
pub fn plus_hours(self, hours: i64) -> Self {
self + Duration::hours(hours)
}
pub fn now_minus_hours(hours: i64) -> Self {
Timestamp::now().minus_hours(hours)
}
pub fn minus_hours(self, hours: i64) -> Self {
self - Duration::hours(hours)
}
pub fn now_plus_minutes(minutes: i64) -> Self {
Timestamp::now().plus_minutes(minutes)
}
pub fn plus_minutes(self, minutes: i64) -> Self {
self + Duration::minutes(minutes)
}
pub fn minus_seconds(self, seconds: i64) -> Self {
self - Duration::seconds(seconds)
}
pub fn plus_seconds(self, seconds: i64) -> Self {
self + Duration::seconds(seconds)
}
pub fn now_minus_seconds(seconds: i64) -> Self {
Timestamp::now().minus_seconds(seconds)
}
pub fn now_plus_seconds(seconds: i64) -> Self {
Timestamp::now().plus_seconds(seconds)
}
pub fn into_rfc3339(self) -> String {
Time::from(self).to_rfc3339()
}
}
impl From<Timestamp> for Time {
fn from(timestamp: Timestamp) -> Self {
Time::new(
Utc.timestamp_opt(timestamp.0, 0)
.single()
.expect("timestamp out-of-range"),
)
}
}
impl From<Time> for Timestamp {
fn from(time: Time) -> Self {
Timestamp(time.timestamp())
}
}
impl From<Timestamp> for i64 {
fn from(t: Timestamp) -> Self {
t.0
}
}
impl ops::Add<Duration> for Timestamp {
type Output = Self;
fn add(self, duration: Duration) -> Self::Output {
Timestamp(self.0 + duration.num_seconds())
}
}
impl ops::AddAssign<Duration> for Timestamp {
fn add_assign(&mut self, duration: Duration) {
self.0 += duration.num_seconds();
}
}
impl ops::Sub<Duration> for Timestamp {
type Output = Self;
fn sub(self, duration: Duration) -> Self::Output {
Timestamp(self.0 - duration.num_seconds())
}
}
impl ops::SubAssign<Duration> for Timestamp {
fn sub_assign(&mut self, duration: Duration) {
self.0 -= duration.num_seconds()
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertAuthInfo {
pub handle: CaHandle,
pub id_cert: IdCertInfo,
pub repo_info: Option<RepoInfo>,
pub parents: Vec<ParentInfo>,
pub resources: ResourceSet,
pub resource_classes: HashMap<ResourceClassName, ResourceClassInfo>,
pub children: Vec<ChildHandle>,
pub suspended_children: Vec<ChildHandle>,
}
impl fmt::Display for CertAuthInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Name: {}", self.handle)?;
writeln!(f)?;
if let Some(repo_info) = self.repo_info.as_ref() {
let base_uri = repo_info.base_uri();
let rrdp_uri = repo_info
.rpki_notify()
.map(|uri| uri.as_str())
.unwrap_or("<none>");
writeln!(f, "Base uri: {base_uri}")?;
writeln!(f, "RRDP uri: {rrdp_uri}")?;
} else {
writeln!(f, "No repository configured.")?;
}
writeln!(f)?;
writeln!(f, "ID cert PEM:\n{}", self.id_cert.pem())?;
writeln!(f, "Hash: {}", self.id_cert.hash)?;
writeln!(f)?;
let resources = &self.resources;
if resources.is_empty() {
writeln!(f, "Total resources: <none>")?;
} else {
writeln!(f, "Total resources:")?;
writeln!(f, " ASNs: {}", resources.asn())?;
writeln!(f, " IPv4: {}", resources.ipv4())?;
writeln!(f, " IPv6: {}", resources.ipv6())?;
}
writeln!(f)?;
writeln!(f, "Parents:")?;
if !self.parents.is_empty() {
for parent in &self.parents {
writeln!(f, "{parent}")?;
}
writeln!(f)?;
} else {
writeln!(f, "<none>")?;
}
for (name, rc) in &self.resource_classes {
writeln!(f, "Resource Class: {name}",)?;
writeln!(f, "Parent: {}", rc.parent_handle)?;
writeln!(f, "{}", rc.keys)?;
}
writeln!(f, "Children:")?;
if !self.children.is_empty() {
for child_handle in &self.children {
writeln!(f, "{child_handle}")?;
}
} else {
writeln!(f, "<none>")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ResourceClassInfo {
pub name_space: String,
pub parent_handle: ParentHandle,
pub keys: ResourceClassKeysInfo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[allow(clippy::large_enum_variant)]
#[serde(rename_all = "snake_case")]
pub enum ResourceClassKeysInfo {
Pending(PendingInfo),
Active(ActiveInfo),
RollPending(RollPendingInfo),
RollNew(RollNewInfo),
RollOld(RollOldInfo),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PendingInfo {
pub pending_key: PendingKeyInfo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ActiveInfo {
pub active_key: CertifiedKeyInfo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RollPendingInfo {
pub pending_key: PendingKeyInfo,
pub active_key: CertifiedKeyInfo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RollNewInfo {
pub new_key: CertifiedKeyInfo,
pub active_key: CertifiedKeyInfo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RollOldInfo {
pub active_key: CertifiedKeyInfo,
pub old_key: CertifiedKeyInfo,
}
impl ResourceClassKeysInfo {
pub fn current_key(&self) -> Option<&CertifiedKeyInfo> {
match &self {
ResourceClassKeysInfo::Active(current) => {
Some(¤t.active_key)
}
ResourceClassKeysInfo::RollPending(pending) => {
Some(&pending.active_key)
}
ResourceClassKeysInfo::RollNew(new) => Some(&new.active_key),
ResourceClassKeysInfo::RollOld(old) => Some(&old.active_key),
_ => None,
}
}
pub fn new_key(&self) -> Option<&CertifiedKeyInfo> {
if let ResourceClassKeysInfo::RollNew(new) = self {
Some(&new.new_key)
} else {
None
}
}
pub fn current_resources(&self) -> Option<&ResourceSet> {
self.current_key().map(|k| &k.incoming_cert.resources)
}
}
impl fmt::Display for ResourceClassKeysInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "State: ")?;
match &self {
ResourceClassKeysInfo::Pending(_) => writeln!(f, "pending")?,
ResourceClassKeysInfo::Active(_) => writeln!(f, "active")?,
ResourceClassKeysInfo::RollPending(_) => {
writeln!(f, "roll phase 1: pending and active key")?
}
ResourceClassKeysInfo::RollNew(_) => {
writeln!(f, "roll phase 2: new and active key")?
}
ResourceClassKeysInfo::RollOld(_) => {
writeln!(f, "roll phase 3: active and old key")?
}
}
if let Some(key) = self.current_key() {
let resources = &key.incoming_cert.resources;
writeln!(f, "Resources:")?;
writeln!(f, " ASNs: {}", resources.asn())?;
writeln!(f, " IPv4: {}", resources.ipv4())?;
writeln!(f, " IPv6: {}", resources.ipv6())?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CaRepoDetails {
pub contact: RepositoryContact,
}
impl fmt::Display for CaRepoDetails {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let rrdp_uri = self.contact.repo_info
.rpki_notify()
.map(|uri| uri.as_str())
.unwrap_or("<none>");
writeln!(f, "Repository Details:")?;
writeln!(
f, " service uri: {}", self.contact.server_info.service_uri
)?;
writeln!(
f,
" key identifier: {}",
self.contact.server_info.public_key.key_identifier()
)?;
writeln!(
f, " base_uri: {}", self.contact.repo_info.base_uri()
)?;
writeln!(f, " rpki_notify: {rrdp_uri}")?;
writeln!(f)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct AllCertAuthIssues {
pub cas: HashMap<CaHandle, CertAuthIssues>,
}
impl fmt::Display for AllCertAuthIssues {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.cas.is_empty() {
writeln!(f, "no issues found")?;
}
else {
for (ca, issues) in &self.cas {
writeln!(f, "Found issue for CA '{ca}':")?;
if let Some(repo_issue) = issues.repo_issue() {
writeln!(f, " Repository Issue: {repo_issue}")?;
}
let parent_issues = issues.parent_issues();
if !parent_issues.is_empty() {
for parent_issue in parent_issues.iter() {
writeln!(
f,
" Parent '{}' has issue: {}",
parent_issue.parent, parent_issue.issue
)?;
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertAuthIssues {
pub repo_issue: Option<ErrorResponse>,
pub parent_issues: Vec<CertAuthParentIssue>,
}
impl CertAuthIssues {
pub fn repo_issue(&self) -> Option<&ErrorResponse> {
self.repo_issue.as_ref()
}
pub fn add_parent_issue(
&mut self,
parent: ParentHandle,
issue: ErrorResponse,
) {
let parent_issue = CertAuthParentIssue { parent, issue };
self.parent_issues.push(parent_issue);
}
pub fn parent_issues(&self) -> &Vec<CertAuthParentIssue> {
&self.parent_issues
}
pub fn is_empty(&self) -> bool {
self.repo_issue.is_none() && self.parent_issues.is_empty()
}
}
impl fmt::Display for CertAuthIssues {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_empty() {
writeln!(f, "no issues found")?;
}
else {
if let Some(repo_issue) = self.repo_issue.as_ref() {
writeln!(f, "Repository Issue: {repo_issue}")?;
}
if !self.parent_issues.is_empty() {
for parent_issue in &self.parent_issues {
writeln!(
f,
"Parent '{}' has issue: {}",
parent_issue.parent, parent_issue.issue
)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertAuthParentIssue {
pub parent: ParentHandle,
pub issue: ErrorResponse,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CertAuthStats {
pub roa_count: usize,
pub child_count: usize,
pub bgp_stats: BgpStats,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct BgpStats {
pub announcements_valid: usize,
pub announcements_invalid_asn: usize,
pub announcements_invalid_length: usize,
pub announcements_disallowed: usize,
pub announcements_not_found: usize,
pub roas_too_permissive: usize,
pub roas_redundant: usize,
pub roas_stale: usize,
pub roas_disallowing: usize,
pub roas_not_held: usize,
pub roas_total: usize,
}
pub type RtaName = String;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RtaList(Vec<RtaName>);
impl RtaList {
pub fn new(list: Vec<RtaName>) -> Self {
RtaList(list)
}
}
impl fmt::Display for RtaList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for name in &self.0 {
writeln!(f, "{name}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RtaPrepResponse(Vec<KeyIdentifier>);
impl RtaPrepResponse {
pub fn new(keys: Vec<KeyIdentifier>) -> Self {
RtaPrepResponse(keys)
}
}
impl From<RtaPrepResponse> for Vec<KeyIdentifier> {
fn from(r: RtaPrepResponse) -> Self {
r.0
}
}
impl fmt::Display for RtaPrepResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Created the following keys")?;
for key in &self.0 {
writeln!(f, " {key}")?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub enum InvalidCert {
CaRepositoryMissing,
RpkiManifestMissing,
Uri(uri::Rsync),
}
impl fmt::Display for InvalidCert {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::CaRepositoryMissing => {
f.write_str(
"CA certificate lacks id-ad-caRepository \
(see section 4.8.8.1 of RFC 6487)"
)
}
Self::RpkiManifestMissing => {
f.write_str(
"CA certificate lacks id-ad-rpkiManifest \
(see section 4.8.8.1 of RFC 6487)"
)
}
Self::Uri(s) => {
write!(f, "Cannot derive filename from URI: {s}")
}
}
}
}
impl std::error::Error for InvalidCert {}
#[derive(Clone, Debug)]
pub struct CertInfoDecodeError(String);
impl fmt::Display for CertInfoDecodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Cannot decode binary certificate: {}", self.0)
}
}
impl std::error::Error for CertInfoDecodeError {}
#[cfg(test)]
mod test {
use bytes::Bytes;
use rpki::crypto::PublicKeyFormat;
use crate::api::ta::TrustAnchorLocator;
use crate::commons::crypto::OpenSslSigner;
use crate::commons::test;
use super::*;
fn base_uri() -> uri::Rsync {
test::rsync("rsync://localhost/repo/ta/")
}
fn rrdp_uri() -> uri::Https {
test::https("https://localhost/rrdp/notification.xml")
}
fn info() -> RepoInfo {
RepoInfo::new(base_uri(), Some(rrdp_uri()))
}
#[test]
fn signed_objects_uri() {
let signed_objects_uri = info().ca_repository("");
assert_eq!(base_uri(), signed_objects_uri)
}
#[test]
fn mft_uri() {
test::test_in_memory(|storage_uri| {
let signer =
OpenSslSigner::build(storage_uri, "dummy", None).unwrap();
let key_id = signer.create_key(PublicKeyFormat::Rsa).unwrap();
let pub_key = signer.get_key_info(&key_id).unwrap();
let mft_uri = info().resolve(
"",
ObjectName::mft_from_ca_key(&pub_key.key_identifier()).as_ref(),
);
let mft_path = mft_uri.relative_to(&base_uri()).unwrap();
assert_eq!(44, mft_path.len());
let name = &mft_path[..40];
hex::decode(name).unwrap();
let ext = &mft_path[40..];
assert_eq!(ext, ".mft");
});
}
#[test]
fn serialize_deserialize_repo_info() {
let info = RepoInfo::new(
test::rsync("rsync://some/module/folder/"),
Some(test::https("https://host/notification.xml")),
);
let json = serde_json::to_string(&info).unwrap();
let deser_info = serde_json::from_str(&json).unwrap();
assert_eq!(info, deser_info);
}
#[test]
fn create_and_display_tal() {
let der = include_bytes!("../../test-resources/ta.cer");
let cert = Cert::decode(Bytes::from_static(der)).unwrap();
let uri = test::https("https://localhost/ta.cer");
let rsync_uri = test::rsync("rsync://localhost/ta/ta.cer");
let tal = TrustAnchorLocator::new(
vec![uri],
rsync_uri,
cert.subject_public_key_info(),
);
let expected_tal = include_str!("../../test-resources/test.tal");
let found_tal = tal.to_string();
assert_eq!(expected_tal, &found_tal);
}
#[test]
fn id_cert_pem_match_openssl() {
let ncc_id = {
let bytes =
include_bytes!("../../test-resources/remote/ncc-id.der");
IdCert::decode(bytes.as_ref()).unwrap()
};
let ncc_id_openssl_pem =
include_str!("../../test-resources/remote/ncc-id.pem");
let ncc_id_pem = IdCertInfo::from(&ncc_id);
assert_eq!(ncc_id_pem.pem().to_string(), ncc_id_openssl_pem);
}
#[test]
fn serde_cert_auth_issues() {
let mut issues = CertAuthIssues::default();
use crate::commons::httpclient;
use crate::commons::error::Error;
issues.repo_issue = Some(
Error::HttpClientError(httpclient::Error::forbidden(
"https://example.com/",
))
.to_error_response(),
);
issues.add_parent_issue(
ParentHandle::from_str("parent").unwrap(),
Error::Rfc6492InvalidCsrSent("invalid csr".to_string())
.to_error_response(),
);
let serialized = serde_json::to_string_pretty(&issues).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
assert_eq!(issues, deserialized);
}
#[test]
fn recognize_suspension_candidate() {
let ca_handle = CaHandle::from_str("ca").unwrap();
let threshold_seconds = 4 * 3600;
fn new_exchange(agent: &str) -> ChildExchange {
let user_agent = if agent.is_empty() {
None
} else {
Some(agent.to_string())
};
ChildExchange {
timestamp: Timestamp::now(),
result: ExchangeResult::Success,
user_agent,
}
}
fn old_exchange(agent: &str) -> ChildExchange {
let user_agent = if agent.is_empty() {
None
} else {
Some(agent.to_string())
};
ChildExchange {
timestamp: Timestamp::now_minus_hours(5),
result: ExchangeResult::Success,
user_agent,
}
}
fn ca_stats_active_no_exchange(
child: &CaHandle,
) -> ChildConnectionStats {
ChildConnectionStats {
handle: child.convert(),
last_exchange: None,
state: ChildState::Active,
}
}
fn ca_stats_active(
child: &CaHandle,
exchange: ChildExchange,
) -> ChildConnectionStats {
ChildConnectionStats {
handle: child.convert(),
last_exchange: Some(exchange),
state: ChildState::Active,
}
}
let new_ca = ca_stats_active_no_exchange(&ca_handle);
let recent_krill_pre_0_9_2 =
ca_stats_active(&ca_handle, new_exchange("krill"));
let recent_krill_post_0_9_1 =
ca_stats_active(&ca_handle, new_exchange("krill/0.9.2-rc2"));
let recent_other_agent =
ca_stats_active(&ca_handle, new_exchange("other"));
let recent_no_agent = ca_stats_active(&ca_handle, new_exchange(""));
let old_krill_pre_0_9_2 =
ca_stats_active(&ca_handle, old_exchange("krill"));
let old_krill_post_0_9_1 =
ca_stats_active(&ca_handle, old_exchange("krill/0.9.2-rc2"));
let old_other_agent =
ca_stats_active(&ca_handle, old_exchange("other"));
let old_no_agent = ca_stats_active(&ca_handle, old_exchange(""));
assert!(!new_ca.is_suspension_candidate(threshold_seconds));
assert!(!recent_krill_pre_0_9_2
.is_suspension_candidate(threshold_seconds));
assert!(!recent_krill_post_0_9_1
.is_suspension_candidate(threshold_seconds));
assert!(
!recent_other_agent.is_suspension_candidate(threshold_seconds)
);
assert!(!recent_no_agent.is_suspension_candidate(threshold_seconds));
assert!(
!old_krill_pre_0_9_2.is_suspension_candidate(threshold_seconds)
);
assert!(!old_other_agent.is_suspension_candidate(threshold_seconds));
assert!(!old_no_agent.is_suspension_candidate(threshold_seconds));
assert!(
old_krill_post_0_9_1.is_suspension_candidate(threshold_seconds)
);
}
#[test]
fn find_sync_candidates() {
let uri = ServiceUri::try_from(
"https://example.com/rfc6492/child/".to_string(),
)
.unwrap();
let five_seconds_ago = Timestamp::now_minus_seconds(5);
let five_mins_ago = Timestamp::now_minus_seconds(300);
let p1_new_parent = ParentHandle::from_str("p1").unwrap();
let p2_new_parent = ParentHandle::from_str("p2").unwrap();
let p3_no_exchange = ParentHandle::from_str("p3").unwrap();
let p4_success = ParentHandle::from_str("p4").unwrap();
let p5_failure = ParentHandle::from_str("p5").unwrap();
let p6_success_long_ago = ParentHandle::from_str("p6").unwrap();
let p3_status_no_exchange = ParentStatus {
last_exchange: None,
last_success: None,
all_resources: ResourceSet::default(),
classes: vec![],
};
let p4_status_success = ParentStatus {
last_exchange: Some(ParentExchange {
timestamp: five_seconds_ago,
uri: uri.clone(),
result: ExchangeResult::Success,
}),
last_success: None,
all_resources: ResourceSet::default(),
classes: vec![],
};
let p5_status_failure = ParentStatus {
last_exchange: Some(ParentExchange {
timestamp: five_seconds_ago,
uri: uri.clone(),
result: ExchangeResult::Failure(ErrorResponse::new(
"err", "err!",
)),
}),
last_success: None,
all_resources: ResourceSet::default(),
classes: vec![],
};
let p6_status_success_long_ago = ParentStatus {
last_exchange: Some(ParentExchange {
timestamp: five_mins_ago,
uri,
result: ExchangeResult::Success,
}),
last_success: None,
all_resources: ResourceSet::default(),
classes: vec![],
};
let mut inner_statuses = HashMap::new();
inner_statuses.insert(p3_no_exchange.clone(), p3_status_no_exchange);
inner_statuses.insert(p4_success.clone(), p4_status_success);
inner_statuses.insert(p5_failure.clone(), p5_status_failure);
inner_statuses
.insert(p6_success_long_ago.clone(), p6_status_success_long_ago);
let parent_statuses = ParentStatuses(inner_statuses);
let ca_parents = vec![
&p1_new_parent,
&p2_new_parent,
&p3_no_exchange,
&p4_success,
&p5_failure,
&p6_success_long_ago,
];
let candidates =
parent_statuses.sync_candidates(ca_parents.clone(), 10);
#[allow(clippy::redundant_clone)] let expected = vec![
p1_new_parent.clone(),
p2_new_parent.clone(),
p3_no_exchange.clone(),
p6_success_long_ago.clone(),
p5_failure.clone(),
p4_success.clone(),
];
assert_eq!(candidates, expected);
let candidates_trimmed =
parent_statuses.sync_candidates(ca_parents, 1);
let expected_trimmed = vec![p1_new_parent];
assert_eq!(candidates_trimmed, expected_trimmed);
}
}