mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-09-12 04:27:41 +02:00
Re-architecting the application to use event sourcing (fixes: #35)
This commit is contained in:
+134
-5
@@ -1,14 +1,143 @@
|
||||
//! Data structures for the API, shared between client and server.
|
||||
pub mod publishers;
|
||||
pub mod publication;
|
||||
pub mod rrdp_data;
|
||||
pub mod publisher_data;
|
||||
pub mod publication_data;
|
||||
pub mod repo_data;
|
||||
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use crate::util::sha256;
|
||||
|
||||
|
||||
//------------ Base64 --------------------------------------------------------
|
||||
|
||||
/// This type contains a base64 encoded structure. The publication protocol
|
||||
/// deals with objects in their base64 encoded form.
|
||||
///
|
||||
/// Note that we store this in a Bytes to make it cheap to clone this.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Base64(Bytes);
|
||||
|
||||
impl Base64 {
|
||||
pub fn from_content(content: &[u8]) -> Self {
|
||||
Base64::from(base64::encode(content))
|
||||
}
|
||||
|
||||
/// Decodes into bytes (e.g. for saving to disk for rcync)
|
||||
pub fn to_bytes(&self) -> Bytes {
|
||||
Bytes::from(base64::decode(&self.0).unwrap())
|
||||
}
|
||||
|
||||
pub fn to_hex_hash(&self) -> String {
|
||||
hex::encode(sha256(&self.to_bytes()))
|
||||
}
|
||||
|
||||
pub fn to_encoded_hash(&self) -> EncodedHash {
|
||||
EncodedHash::from(self.to_hex_hash())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Base64 {
|
||||
fn as_ref(&self) -> &str {
|
||||
use std::str;
|
||||
str::from_utf8(&self.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Base64 {
|
||||
fn from(s: String) -> Self {
|
||||
Base64(Bytes::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ToString for Base64 {
|
||||
fn to_string(&self) -> String {
|
||||
unsafe {
|
||||
String::from_utf8_unchecked(self.0.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Base64 {
|
||||
fn serialize<S>(
|
||||
&self, serializer: S
|
||||
) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Base64 {
|
||||
fn deserialize<D>(
|
||||
deserializer: D
|
||||
) -> Result<Base64, D::Error> where D: Deserializer<'de> {
|
||||
let string = String::deserialize(deserializer)?;
|
||||
Ok(Base64::from(string))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ EncodedHash ---------------------------------------------------
|
||||
|
||||
/// This type contains a hex encoded sha256 hash.
|
||||
///
|
||||
/// Note that we store this in a Bytes for cheap cloning.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct EncodedHash(Bytes);
|
||||
|
||||
impl EncodedHash {
|
||||
pub fn from_content(content: &[u8]) -> Self {
|
||||
let sha256 = sha256(content);
|
||||
let hex = hex::encode(sha256);
|
||||
EncodedHash::from(hex)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EncodedHash {
|
||||
fn as_ref(&self) -> &str {
|
||||
use std::str;
|
||||
str::from_utf8(&self.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for EncodedHash {
|
||||
fn from(s: String) -> Self {
|
||||
EncodedHash(Bytes::from(s.to_lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for EncodedHash {
|
||||
fn to_string(&self) -> String {
|
||||
unsafe {
|
||||
String::from_utf8_unchecked(self.0.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for EncodedHash {
|
||||
fn serialize<S>(
|
||||
&self, serializer: S
|
||||
) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EncodedHash {
|
||||
fn deserialize<D>(
|
||||
deserializer: D
|
||||
) -> Result<EncodedHash, D::Error> where D: Deserializer<'de> {
|
||||
let string = String::deserialize(deserializer)?;
|
||||
Ok(EncodedHash::from(string))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------ Link ----------------------------------------------------------
|
||||
|
||||
/// Defines a link element to include as part of a links array in a Json
|
||||
/// response.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Link<'a> {
|
||||
rel: &'a str,
|
||||
pub struct Link {
|
||||
rel: String,
|
||||
link: String
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! Support for requests sent to the Json API
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::api::{ Base64, EncodedHash };
|
||||
use crate::util::ext_serde;
|
||||
use crate::util::file::CurrentFile;
|
||||
use crate::util::sha256;
|
||||
|
||||
|
||||
//------------ PublishRequest ------------------------------------------------
|
||||
@@ -51,6 +50,10 @@ impl PublishDelta {
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool { self.len() == 0 }
|
||||
|
||||
pub fn unwrap(self) -> (Vec<Publish>, Vec<Update>, Vec<Withdraw>) {
|
||||
(self.publishes, self.updates, self.withdraws)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,18 +107,15 @@ pub struct Publish {
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
content: Bytes
|
||||
content: Base64
|
||||
}
|
||||
|
||||
impl Publish {
|
||||
pub fn new(tag: Option<String>, uri: uri::Rsync, content: Bytes) -> Self {
|
||||
pub fn new(tag: Option<String>, uri: uri::Rsync, content: Base64) -> Self {
|
||||
Publish { tag, uri, content }
|
||||
}
|
||||
pub fn with_hash_tag(uri: uri::Rsync, content: Bytes) -> Self {
|
||||
let tag = Some(hex::encode(sha256(&content)));
|
||||
pub fn with_hash_tag(uri: uri::Rsync, content: Base64) -> Self {
|
||||
let tag = Some(content.to_hex_hash());
|
||||
Publish { tag, uri, content }
|
||||
}
|
||||
|
||||
@@ -127,7 +127,11 @@ impl Publish {
|
||||
}
|
||||
}
|
||||
pub fn uri(&self) -> &uri::Rsync{ &self.uri}
|
||||
pub fn content(&self) -> &Bytes{ &self.content }
|
||||
pub fn content(&self) -> &Base64{ &self.content }
|
||||
|
||||
pub fn unwrap(self) -> (Option<String>, uri::Rsync, Base64) {
|
||||
(self.tag, self.uri, self.content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,32 +149,26 @@ pub struct Update {
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
content: Bytes,
|
||||
content: Base64,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
hash: Bytes,
|
||||
hash: EncodedHash,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
pub fn new(
|
||||
tag: Option<String>,
|
||||
uri: uri::Rsync,
|
||||
content: Bytes,
|
||||
old_hash: Bytes
|
||||
content: Base64,
|
||||
old_hash: EncodedHash
|
||||
) -> Self {
|
||||
Update { tag, uri, content, hash: old_hash }
|
||||
}
|
||||
pub fn with_hash_tag(
|
||||
uri: uri::Rsync,
|
||||
content: Bytes,
|
||||
old_hash: Bytes
|
||||
content: Base64,
|
||||
old_hash: EncodedHash
|
||||
) -> Self {
|
||||
let tag = Some(hex::encode(sha256(&content)));
|
||||
let tag = Some(content.to_hex_hash());
|
||||
Update { tag, uri, content, hash: old_hash }
|
||||
}
|
||||
|
||||
@@ -182,8 +180,12 @@ impl Update {
|
||||
}
|
||||
}
|
||||
pub fn uri(&self) -> &uri::Rsync { &self.uri}
|
||||
pub fn content(&self) -> &Bytes { &self.content }
|
||||
pub fn hash(&self) -> &Bytes { &self.hash }
|
||||
pub fn content(&self) -> &Base64 { &self.content }
|
||||
pub fn hash(&self) -> &EncodedHash { &self.hash }
|
||||
|
||||
pub fn unwrap(self) -> (Option<String>, uri::Rsync, Base64, EncodedHash) {
|
||||
(self.tag, self.uri, self.content, self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,19 +203,16 @@ pub struct Withdraw {
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
hash: Bytes,
|
||||
hash: EncodedHash,
|
||||
}
|
||||
|
||||
impl Withdraw {
|
||||
pub fn new(tag: Option<String>, uri: uri::Rsync, hash: Bytes) -> Self {
|
||||
pub fn new(tag: Option<String>, uri: uri::Rsync, hash: EncodedHash) -> Self {
|
||||
Withdraw { tag, uri, hash }
|
||||
}
|
||||
|
||||
pub fn with_hash_tag(uri: uri::Rsync, hash: Bytes) -> Self {
|
||||
let tag = Some(hex::encode(&hash));
|
||||
pub fn with_hash_tag(uri: uri::Rsync, hash: EncodedHash) -> Self {
|
||||
let tag = Some(hash.to_string());
|
||||
Withdraw { tag, uri, hash }
|
||||
}
|
||||
|
||||
@@ -233,7 +232,11 @@ impl Withdraw {
|
||||
}
|
||||
}
|
||||
pub fn uri(&self) -> &uri::Rsync { &self.uri}
|
||||
pub fn hash(&self) -> &Bytes { &self.hash }
|
||||
pub fn hash(&self) -> &EncodedHash { &self.hash }
|
||||
|
||||
pub fn unwrap(self) -> (Option<String>, uri::Rsync, EncodedHash) {
|
||||
(self.tag, self.uri, self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
//------------ PublishReply --------------------------------------------------
|
||||
@@ -281,21 +284,14 @@ pub struct ListElement {
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
/// The sha-256 hash of the file (as is used on the RPKI manifests and
|
||||
/// in the publication protocol for list, update and withdraw). Saving
|
||||
/// this rather than calculating on demand seems a small price for some
|
||||
/// performance gain.
|
||||
hash: Bytes
|
||||
hash: EncodedHash
|
||||
}
|
||||
|
||||
impl ListElement {
|
||||
pub fn new(uri: uri::Rsync, hash: Bytes) -> Self {
|
||||
pub fn new(uri: uri::Rsync, hash: EncodedHash) -> Self {
|
||||
ListElement { uri, hash }
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &uri::Rsync { &self.uri }
|
||||
pub fn hash(&self) -> &Bytes { &self.hash}
|
||||
pub fn hash(&self) -> &EncodedHash { &self.hash }
|
||||
}
|
||||
@@ -2,13 +2,21 @@
|
||||
//!
|
||||
//! i.e. this is stuff the the server needs to serialize only, so typically
|
||||
//! we can work with references here.
|
||||
use std::sync::Arc;
|
||||
use rpki::uri;
|
||||
use crate::api::Link;
|
||||
use crate::eventsourcing::AggregateId;
|
||||
use crate::krilld::pubd::publishers::Publisher;
|
||||
use crate::remote::id::IdCert;
|
||||
use crate::util::ext_serde;
|
||||
|
||||
|
||||
//------------ PublisherHandle -----------------------------------------------
|
||||
|
||||
/// A type for referring to publishers, both in the api as well as to the
|
||||
/// aggregates.
|
||||
pub type PublisherHandle = AggregateId;
|
||||
|
||||
|
||||
//------------ CmsAuthData ---------------------------------------------------
|
||||
|
||||
/// This type contains the data needed for handling RFC8183 requests/responses,
|
||||
@@ -51,11 +59,12 @@ impl PartialEq for CmsAuthData {
|
||||
impl Eq for CmsAuthData {}
|
||||
|
||||
|
||||
//------------ Publisher -----------------------------------------------------
|
||||
//------------ PublisherRequest ----------------------------------------------
|
||||
|
||||
/// This type defines Publisher CAs that are allowed to publish.
|
||||
/// This type defines request for a new Publisher (CA that is allowed to
|
||||
/// publish).
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Publisher {
|
||||
pub struct PublisherRequest {
|
||||
handle: String,
|
||||
|
||||
/// The token used by the API
|
||||
@@ -69,14 +78,14 @@ pub struct Publisher {
|
||||
cms_auth_data: Option<CmsAuthData>
|
||||
}
|
||||
|
||||
impl Publisher {
|
||||
impl PublisherRequest {
|
||||
pub fn new(
|
||||
handle: String,
|
||||
token: String,
|
||||
base_uri: uri::Rsync,
|
||||
rfc8181: Option<CmsAuthData>
|
||||
) -> Self {
|
||||
Publisher {
|
||||
PublisherRequest {
|
||||
handle,
|
||||
token,
|
||||
base_uri,
|
||||
@@ -85,7 +94,7 @@ impl Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
impl Publisher {
|
||||
impl PublisherRequest {
|
||||
pub fn handle(&self) -> &String {
|
||||
&self.handle
|
||||
}
|
||||
@@ -101,17 +110,22 @@ impl Publisher {
|
||||
pub fn cms_auth_data(&self) -> &Option<CmsAuthData> {
|
||||
&self.cms_auth_data
|
||||
}
|
||||
|
||||
/// Return all the values (handle, token, base_uri, rfc8181opt).
|
||||
pub fn unwrap(self) -> (String, String, uri::Rsync, Option<CmsAuthData>) {
|
||||
(self.handle, self.token, self.base_uri, self.cms_auth_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Publisher {
|
||||
fn eq(&self, other: &Publisher) -> bool {
|
||||
impl PartialEq for PublisherRequest {
|
||||
fn eq(&self, other: &PublisherRequest) -> bool {
|
||||
self.handle == other.handle &&
|
||||
self.base_uri == other.base_uri &&
|
||||
self.cms_auth_data == other.cms_auth_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Publisher {}
|
||||
impl Eq for PublisherRequest {}
|
||||
|
||||
|
||||
//------------ PublisherSummaryInfo ------------------------------------------
|
||||
@@ -119,33 +133,25 @@ impl Eq for Publisher {}
|
||||
/// Defines a summary of publisher information to be used in the publisher
|
||||
/// list.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PublisherSummaryInfo<'a> {
|
||||
id: &'a str,
|
||||
links: Vec<Link<'a>>
|
||||
pub struct PublisherSummaryInfo {
|
||||
id: String,
|
||||
links: Vec<Link>
|
||||
}
|
||||
|
||||
impl<'a> PublisherSummaryInfo<'a> {
|
||||
impl PublisherSummaryInfo {
|
||||
pub fn from(
|
||||
publisher: &'a Publisher,
|
||||
path_publishers: &'a str
|
||||
) -> PublisherSummaryInfo<'a> {
|
||||
let id = publisher.handle().as_str();
|
||||
handle: &PublisherHandle,
|
||||
path_publishers: &str
|
||||
) -> PublisherSummaryInfo {
|
||||
let mut links = Vec::new();
|
||||
|
||||
let response_link = Link {
|
||||
rel: "response.xml",
|
||||
link: format!("{}/{}/response.xml", path_publishers, id)
|
||||
};
|
||||
let self_link = Link {
|
||||
rel: "self",
|
||||
link: format!("{}/{}", path_publishers, id)
|
||||
rel: "self".to_string(),
|
||||
link: format!("{}/{}", path_publishers, handle.as_ref())
|
||||
};
|
||||
|
||||
links.push(response_link);
|
||||
links.push(self_link);
|
||||
|
||||
PublisherSummaryInfo {
|
||||
id,
|
||||
id: handle.to_string(),
|
||||
links
|
||||
}
|
||||
}
|
||||
@@ -156,15 +162,15 @@ impl<'a> PublisherSummaryInfo<'a> {
|
||||
|
||||
/// This type represents a list of (all) current publishers to show in the API
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PublisherList<'a> {
|
||||
publishers: Vec<PublisherSummaryInfo<'a>>
|
||||
pub struct PublisherList {
|
||||
publishers: Vec<PublisherSummaryInfo>
|
||||
}
|
||||
|
||||
impl<'a> PublisherList<'a> {
|
||||
pub fn from(
|
||||
publishers: &'a[Arc<Publisher>],
|
||||
path_publishers: &'a str
|
||||
) -> PublisherList<'a> {
|
||||
impl PublisherList {
|
||||
pub fn build(
|
||||
publishers: &[PublisherHandle],
|
||||
path_publishers: &str
|
||||
) -> PublisherList {
|
||||
let publishers: Vec<PublisherSummaryInfo> = publishers.iter().map(|p|
|
||||
PublisherSummaryInfo::from(&p, path_publishers)
|
||||
).collect();
|
||||
@@ -194,24 +200,27 @@ pub struct Rfc8181Details<'a> {
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PublisherDetails<'a> {
|
||||
publisher_handle: &'a str,
|
||||
handle: &'a str,
|
||||
|
||||
retired: bool,
|
||||
|
||||
#[serde(serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
base_uri: &'a uri::Rsync,
|
||||
|
||||
rfc8181: Option<Rfc8181Details<'a>>,
|
||||
|
||||
links: Vec<Link<'a>>
|
||||
links: Vec<Link>
|
||||
}
|
||||
|
||||
impl<'a> PublisherDetails<'a> {
|
||||
pub fn from(
|
||||
publisher: &'a Arc<Publisher>,
|
||||
publisher: &'a Publisher,
|
||||
path_publishers: &'a str,
|
||||
base_service_uri: &uri::Http
|
||||
) -> PublisherDetails<'a> {
|
||||
let handle = publisher.handle().as_str();
|
||||
let handle = publisher.id().as_ref();
|
||||
let base_uri = publisher.base_uri();
|
||||
let retired = publisher.retired();
|
||||
|
||||
// Derive the RFC8181 service URI.
|
||||
let service_uri = format!("{}{}", base_service_uri, handle);
|
||||
@@ -229,12 +238,13 @@ impl<'a> PublisherDetails<'a> {
|
||||
|
||||
let mut links = Vec::new();
|
||||
links.push(Link {
|
||||
rel: "response.xml",
|
||||
rel: "response.xml".to_string(),
|
||||
link: format!("{}/{}/response.xml", path_publishers, handle)
|
||||
});
|
||||
|
||||
PublisherDetails {
|
||||
publisher_handle: handle,
|
||||
handle,
|
||||
retired,
|
||||
base_uri,
|
||||
rfc8181,
|
||||
links
|
||||
@@ -0,0 +1,671 @@
|
||||
//! Data objects used in the (RRDP) repository. I.e. the publish, update, and
|
||||
//! withdraw elements, as well as the notification, snapshot and delta file
|
||||
//! definitions.
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::api::publication_data;
|
||||
use crate::api::Base64;
|
||||
use crate::api::EncodedHash;
|
||||
use crate::util::ext_serde;
|
||||
use crate::util::file;
|
||||
use crate::util::Time;
|
||||
use crate::util::xml::XmlWriter;
|
||||
|
||||
|
||||
const VERSION: &str = "1";
|
||||
const NS: &str = "http://www.ripe.net/rpki/rrdp";
|
||||
|
||||
//------------ PublishElement ------------------------------------------------
|
||||
|
||||
/// The publishes as used in the RRDP protocol.
|
||||
///
|
||||
/// Note that the difference with the publication protocol is the absence of
|
||||
/// the tag.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PublishElement {
|
||||
base64: Base64,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync
|
||||
}
|
||||
|
||||
impl PublishElement {
|
||||
pub fn new(base64: Base64, uri: uri::Rsync) -> Self {
|
||||
PublishElement { base64, uri }
|
||||
}
|
||||
|
||||
pub fn base64(&self) -> &Base64 { &self.base64 }
|
||||
pub fn uri(&self) -> &uri::Rsync { &self.uri }
|
||||
}
|
||||
|
||||
impl From<publication_data::Publish> for PublishElement {
|
||||
fn from(p: publication_data::Publish) -> Self {
|
||||
let (_tag, uri, base64) = p.unwrap();
|
||||
PublishElement { uri, base64 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ UpdateElement -------------------------------------------------
|
||||
|
||||
/// The updates as used in the RRDP protocol.
|
||||
///
|
||||
/// Note that the difference with the publication protocol is the absence of
|
||||
/// the tag.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UpdateElement {
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
hash: EncodedHash,
|
||||
base64: Base64
|
||||
}
|
||||
|
||||
impl UpdateElement {
|
||||
pub fn uri(&self) -> &uri::Rsync { &self.uri }
|
||||
pub fn hash(&self) -> &EncodedHash { &self.hash }
|
||||
pub fn base64(&self) -> &Base64 { &self.base64 }
|
||||
}
|
||||
|
||||
impl From<publication_data::Update> for UpdateElement {
|
||||
fn from(u: publication_data::Update) -> Self {
|
||||
let (_tag, uri, base64, hash) = u.unwrap();
|
||||
UpdateElement { uri, base64, hash }
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<PublishElement> for UpdateElement {
|
||||
fn into(self) -> PublishElement {
|
||||
PublishElement { uri: self.uri, base64: self.base64 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ WithdrawElement -----------------------------------------------
|
||||
|
||||
/// The withdraws as used in the RRDP protocol.
|
||||
///
|
||||
/// Note that the difference with the publication protocol is the absence of
|
||||
/// the tag.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct WithdrawElement {
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
hash: EncodedHash
|
||||
}
|
||||
|
||||
impl WithdrawElement {
|
||||
pub fn uri(&self) -> &uri::Rsync { &self.uri }
|
||||
pub fn hash(&self) -> &EncodedHash { &self.hash }
|
||||
}
|
||||
|
||||
impl From<publication_data::Withdraw> for WithdrawElement {
|
||||
fn from(w: publication_data::Withdraw) -> Self {
|
||||
let (_tag, uri, hash) = w.unwrap();
|
||||
WithdrawElement { uri, hash }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Notification {
|
||||
session: String,
|
||||
serial: u64,
|
||||
time: Time,
|
||||
snapshot: SnapshotRef,
|
||||
deltas: Vec<DeltaRef>,
|
||||
old_refs: Vec<(Time, FileRef)>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct NotificationUpdate {
|
||||
time: Time,
|
||||
session: Option<String>,
|
||||
snapshot: SnapshotRef,
|
||||
delta: DeltaRef,
|
||||
last_delta: u64
|
||||
}
|
||||
|
||||
impl NotificationUpdate {
|
||||
pub fn new(
|
||||
time: Time,
|
||||
session: Option<String>,
|
||||
snapshot: SnapshotRef,
|
||||
delta: DeltaRef,
|
||||
last_delta: u64
|
||||
) -> Self {
|
||||
NotificationUpdate { time, session, snapshot, delta, last_delta }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct NotificationCreate {
|
||||
session: String,
|
||||
snapshot: SnapshotRef
|
||||
}
|
||||
|
||||
impl NotificationUpdate {
|
||||
pub fn unwrap(self) -> (Time, Option<String>, SnapshotRef, DeltaRef, u64) {
|
||||
(self.time, self.session, self.snapshot, self.delta, self.last_delta)
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn old_refs(&self) -> &Vec<(Time, FileRef)> {
|
||||
&self.old_refs
|
||||
}
|
||||
|
||||
pub fn update(&mut self, update: NotificationUpdate) {
|
||||
let (time, session_opt, snapshot, delta, last_delta) = update.unwrap();
|
||||
if let Some(session) = session_opt {
|
||||
self.session = session;
|
||||
}
|
||||
|
||||
self.serial += 1;
|
||||
self.time = time;
|
||||
|
||||
let mut refs_to_retire = vec![];
|
||||
|
||||
refs_to_retire.push((Time::now(), self.snapshot.clone()));
|
||||
self.snapshot = snapshot;
|
||||
|
||||
for d in &self.deltas {
|
||||
if d.serial < last_delta {
|
||||
refs_to_retire.push((Time::now(), d.file_ref.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
self.deltas.insert(0, delta);
|
||||
self.deltas.retain(|delta| delta.serial >= last_delta);
|
||||
self.old_refs.append(&mut refs_to_retire);
|
||||
}
|
||||
|
||||
/// Cleans up all old references from before the given time.
|
||||
pub fn clean_up(&mut self, t: Time) {
|
||||
self.old_refs.retain(|old_ref| {! old_ref.0.on_or_before(&t)})
|
||||
}
|
||||
|
||||
pub fn create(session: String, snapshot: SnapshotRef) -> Self {
|
||||
Notification {
|
||||
session,
|
||||
serial: 0,
|
||||
time: Time::now(),
|
||||
snapshot,
|
||||
deltas: vec![],
|
||||
old_refs: vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_xml(&self, path: &PathBuf) -> Result<(), io::Error> {
|
||||
debug!("Writing notification file: {}", path.to_string_lossy());
|
||||
let mut file = file::create_file_with_path(&path)?;
|
||||
|
||||
XmlWriter::encode_to_file(& mut file, |w| {
|
||||
|
||||
let a = [
|
||||
("xmlns", NS),
|
||||
("version", VERSION),
|
||||
("session_id", self.session.as_ref()),
|
||||
("serial", &format!("{}", self.serial)),
|
||||
];
|
||||
|
||||
w.put_element(
|
||||
"notification",
|
||||
Some(&a),
|
||||
|w| {
|
||||
{
|
||||
// snapshot ref
|
||||
let uri = self.snapshot.uri.to_string();
|
||||
let a = [
|
||||
("uri", uri.as_str()),
|
||||
("hash", self.snapshot.hash.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"snapshot",
|
||||
Some(&a),
|
||||
|w| { w.empty() }
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
// delta refs
|
||||
for delta in &self.deltas {
|
||||
let serial = format!("{}", delta.serial);
|
||||
let uri = delta.file_ref.uri.to_string();
|
||||
let a = [
|
||||
("serial", serial.as_ref()),
|
||||
("uri", uri.as_str()),
|
||||
("hash", delta.file_ref.hash.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"delta",
|
||||
Some(&a),
|
||||
|w| { w.empty() }
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct FileRef {
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_http_uri",
|
||||
serialize_with = "ext_serde::ser_http_uri")]
|
||||
uri: uri::Http,
|
||||
path: PathBuf,
|
||||
hash: EncodedHash,
|
||||
}
|
||||
|
||||
impl FileRef {
|
||||
pub fn new(uri: uri::Http, path: PathBuf, hash: EncodedHash) -> Self {
|
||||
FileRef { uri, path, hash }
|
||||
}
|
||||
pub fn uri(&self) -> &uri::Http { &self.uri }
|
||||
pub fn path(&self) -> &PathBuf { &self.path }
|
||||
pub fn hash(&self) -> &EncodedHash { &self.hash }
|
||||
}
|
||||
|
||||
pub type SnapshotRef = FileRef;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DeltaRef {
|
||||
serial: u64,
|
||||
file_ref: FileRef
|
||||
}
|
||||
|
||||
impl DeltaRef {
|
||||
pub fn new(serial: u64, file_ref: FileRef) -> Self {
|
||||
DeltaRef { serial, file_ref }
|
||||
}
|
||||
|
||||
pub fn serial(&self) -> u64 { self.serial }
|
||||
}
|
||||
|
||||
impl AsRef<FileRef> for DeltaRef {
|
||||
fn as_ref(&self) -> &FileRef {
|
||||
&self.file_ref
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ CurrentObjects ------------------------------------------------
|
||||
|
||||
/// Defines a current set of published elements.
|
||||
///
|
||||
// Note this is mapped internally for speedy access, by hash, rather than uri
|
||||
// for two reasons:
|
||||
// a) URIs in RPKI may change in future
|
||||
// b) The publish element as it appears in an RFC8182 snapshot.xml includes
|
||||
// the uri and the base64, but not the hash. So keeping the actual elements
|
||||
// around means we can be more efficient in producing that output.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CurrentObjects(HashMap<EncodedHash, PublishElement>);
|
||||
|
||||
impl Default for CurrentObjects {
|
||||
fn default() -> Self {
|
||||
CurrentObjects(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentObjects {
|
||||
fn elements(&self) -> Vec<&PublishElement> {
|
||||
let mut res = vec![];
|
||||
for el in self.0.values() {
|
||||
res.push(el)
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ VerificationError ---------------------------------------------
|
||||
|
||||
/// Issues with relation to verifying deltas.
|
||||
#[derive(Clone, Debug, Display)]
|
||||
pub enum VerificationError {
|
||||
#[display(fmt="Publishing ({}) outside of jail URI ({}) is not allowed.", _0, _1)]
|
||||
UriOutsideJail(uri::Rsync, uri::Rsync),
|
||||
|
||||
#[display(fmt="File already exists for uri (use update!): {}", _0)]
|
||||
ObjectAlreadyPresent(uri::Rsync),
|
||||
|
||||
#[display(fmt="File does not match hash at uri: {}", _0)]
|
||||
NoObjectForHashAndOrUri(uri::Rsync),
|
||||
}
|
||||
|
||||
impl VerificationError {
|
||||
fn outside(jail: &uri::Rsync, uri: &uri::Rsync) -> Self {
|
||||
VerificationError::UriOutsideJail(uri.clone(), jail.clone())
|
||||
}
|
||||
|
||||
fn present(uri: &uri::Rsync) -> Self {
|
||||
VerificationError::ObjectAlreadyPresent(uri.clone())
|
||||
}
|
||||
|
||||
fn no_match(uri: &uri::Rsync) -> Self {
|
||||
VerificationError::NoObjectForHashAndOrUri(uri.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentObjects {
|
||||
|
||||
fn has_match(
|
||||
&self,
|
||||
hash: &EncodedHash,
|
||||
uri: &uri::Rsync
|
||||
) -> bool {
|
||||
match self.0.get(hash) {
|
||||
Some(el) => el.uri() == uri,
|
||||
None => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_delta(
|
||||
&self,
|
||||
delta: &DeltaElements,
|
||||
jail: &uri::Rsync
|
||||
) -> Result<(), VerificationError> {
|
||||
|
||||
for p in delta.publishes() {
|
||||
if ! jail.is_parent_of(p.uri()) {
|
||||
return Err(VerificationError::outside(jail, p.uri()))
|
||||
}
|
||||
let hash = p.base64().to_encoded_hash();
|
||||
if self.0.contains_key(&hash) {
|
||||
return Err(VerificationError::present(p.uri()))
|
||||
}
|
||||
}
|
||||
|
||||
for u in delta.updates() {
|
||||
if ! self.has_match(u.hash(), u.uri()) {
|
||||
return Err(VerificationError::no_match(u.uri()));
|
||||
}
|
||||
}
|
||||
|
||||
for w in delta.withdraws() {
|
||||
if ! self.has_match(w.hash(), w.uri()) {
|
||||
return Err(VerificationError::no_match(w.uri()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies a delta to CurrentObjects. This will asume that the delta
|
||||
/// contains only valid updates for this delta.
|
||||
pub fn apply_delta(&mut self, delta: DeltaElements) {
|
||||
let (publishes, updates, withdraws) = delta.unwrap();
|
||||
|
||||
for p in publishes {
|
||||
let hash = p.base64().to_encoded_hash();
|
||||
self.0.insert(hash, p);
|
||||
}
|
||||
|
||||
for u in updates {
|
||||
self.0.remove(u.hash());
|
||||
let p: PublishElement = u.into();
|
||||
let hash = p.base64().to_encoded_hash();
|
||||
self.0.insert(hash, p);
|
||||
}
|
||||
|
||||
for w in withdraws {
|
||||
self.0.remove(w.hash());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize { self.0.len() }
|
||||
|
||||
pub fn is_empty(&self) -> bool { self.0.is_empty() }
|
||||
|
||||
pub fn to_list_reply(&self) -> publication_data::ListReply {
|
||||
let elements = self.0.iter().map(|el| {
|
||||
let hash = el.0.clone();
|
||||
let uri = el.1.uri().clone();
|
||||
publication_data::ListElement::new(uri, hash)
|
||||
}).collect();
|
||||
|
||||
publication_data::ListReply::new(elements)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ Snapshot ------------------------------------------------------
|
||||
|
||||
/// A structure to contain the RRDP snapshot data.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Snapshot {
|
||||
session: String,
|
||||
serial: u64,
|
||||
current_objects: CurrentObjects
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn new(session: String) -> Self {
|
||||
let current_objects = CurrentObjects::default();
|
||||
Snapshot { session, serial: 0, current_objects }
|
||||
}
|
||||
|
||||
pub fn apply_delta(&mut self, delta: Delta) {
|
||||
let (session, serial, elements) = delta.unwrap();
|
||||
self.session = session;
|
||||
self.serial = serial;
|
||||
self.current_objects.apply_delta(elements)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize { self.current_objects.len() }
|
||||
|
||||
pub fn is_empty(&self) -> bool { self.current_objects.is_empty() }
|
||||
|
||||
pub fn write_xml(&self, path: &PathBuf) -> Result<EncodedHash, io::Error> {
|
||||
let vec = XmlWriter::encode_vec(|w| {
|
||||
let a = [
|
||||
("xmlns", NS),
|
||||
("version", VERSION),
|
||||
("session_id", self.session.as_ref()),
|
||||
("serial", &format!("{}", self.serial)),
|
||||
];
|
||||
|
||||
w.put_element(
|
||||
"snapshot",
|
||||
Some(&a),
|
||||
|w| {
|
||||
for el in self.current_objects.elements() {
|
||||
let uri = el.uri.to_string();
|
||||
let atr = [ ("uri", uri.as_ref())];
|
||||
w.put_element(
|
||||
"publish",
|
||||
Some(&atr),
|
||||
|w| {
|
||||
w.put_text(el.base64.as_ref())
|
||||
}
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
});
|
||||
let bytes = Bytes::from(vec);
|
||||
|
||||
file::save(&bytes, path)?;
|
||||
let hash = EncodedHash::from_content(&bytes);
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ DeltaElements -------------------------------------------------
|
||||
|
||||
/// Defines the elements for an RRDP delta.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DeltaElements {
|
||||
publishes: Vec<PublishElement>,
|
||||
updates: Vec<UpdateElement>,
|
||||
withdraws: Vec<WithdrawElement>
|
||||
}
|
||||
|
||||
impl From<publication_data::PublishDelta> for DeltaElements {
|
||||
fn from(d: publication_data::PublishDelta) -> Self {
|
||||
let (pbls, upds, wdrs) = d.unwrap();
|
||||
|
||||
let publishes = pbls.into_iter().map(PublishElement::from).collect();
|
||||
let updates = upds.into_iter().map(UpdateElement::from).collect();
|
||||
let withdraws = wdrs.into_iter().map(WithdrawElement::from).collect();
|
||||
|
||||
DeltaElements { publishes, updates, withdraws }
|
||||
}
|
||||
}
|
||||
|
||||
impl DeltaElements {
|
||||
pub fn unwrap(
|
||||
self
|
||||
) -> (Vec<PublishElement>, Vec<UpdateElement>, Vec<WithdrawElement>) {
|
||||
(self.publishes, self.updates, self.withdraws)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.publishes.len() + self.updates.len() + self.withdraws.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn publishes(&self) -> &Vec<PublishElement> {
|
||||
&self.publishes
|
||||
}
|
||||
|
||||
pub fn updates(&self) -> &Vec<UpdateElement> {
|
||||
&self.updates
|
||||
}
|
||||
|
||||
pub fn withdraws(&self) -> &Vec<WithdrawElement> {
|
||||
&self.withdraws
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ Delta ---------------------------------------------------------
|
||||
|
||||
/// Defines an RRDP delta.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Delta {
|
||||
session: String,
|
||||
serial: u64,
|
||||
time: Time,
|
||||
elements: DeltaElements
|
||||
}
|
||||
|
||||
impl Delta {
|
||||
pub fn new(
|
||||
session: String,
|
||||
serial: u64,
|
||||
elements: DeltaElements
|
||||
) -> Self {
|
||||
Delta { session, time: Time::now(), serial, elements }
|
||||
}
|
||||
|
||||
pub fn session(&self) -> &str { &self.session }
|
||||
pub fn serial(&self) -> u64 { self.serial }
|
||||
pub fn time(&self) -> &Time { &self.time }
|
||||
pub fn elements(&self) -> &DeltaElements { &self.elements }
|
||||
|
||||
/// Total number of elements
|
||||
///
|
||||
/// This is a cheap approximation of the size of the delta that can help
|
||||
/// in determining the choice of how many deltas to include in a
|
||||
/// notification file.
|
||||
pub fn len(&self) -> usize { self.elements.len() }
|
||||
|
||||
pub fn is_empty(&self) -> bool { self.elements.is_empty() }
|
||||
|
||||
pub fn unwrap(self) -> (String, u64, DeltaElements) {
|
||||
(self.session, self.serial, self.elements)
|
||||
}
|
||||
|
||||
pub fn write_xml(&self, path: &PathBuf) -> Result<EncodedHash, io::Error> {
|
||||
|
||||
let vec = XmlWriter::encode_vec(|w| {
|
||||
let a = [
|
||||
("xmlns", NS),
|
||||
("version", VERSION),
|
||||
("session_id", self.session.as_ref()),
|
||||
("serial", &format!("{}", self.serial)),
|
||||
];
|
||||
|
||||
w.put_element(
|
||||
"delta",
|
||||
Some(&a),
|
||||
|w| {
|
||||
for el in &self.elements.publishes {
|
||||
let uri = el.uri.to_string();
|
||||
let atr = [ ("uri", uri.as_ref())];
|
||||
w.put_element(
|
||||
"publish",
|
||||
Some(&atr),
|
||||
|w| {
|
||||
w.put_text(el.base64.as_ref())
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
for el in &self.elements.updates {
|
||||
let uri = el.uri.to_string();
|
||||
let atr = [
|
||||
("uri", uri.as_ref()),
|
||||
("hash", el.hash.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"publish",
|
||||
Some(&atr),
|
||||
|w| {
|
||||
w.put_text(el.base64.as_ref())
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
for el in &self.elements.withdraws {
|
||||
let uri = el.uri.to_string();
|
||||
let atr = [
|
||||
("uri", uri.as_ref()),
|
||||
("hash", el.hash.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"withdraw",
|
||||
Some(&atr),
|
||||
|w| { w.empty() }
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
let bytes = Bytes::from(vec);
|
||||
file::save(&bytes, &path)?;
|
||||
let hash = EncodedHash::from_content(&bytes);
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,537 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::fs::File;
|
||||
use std::num::ParseIntError;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::util::xml::{AttributesError, XmlReader, XmlReaderErr, XmlWriter};
|
||||
use crate::util::file::{self, RecursorError};
|
||||
use crate::util::ext_serde;
|
||||
use util::sha256;
|
||||
|
||||
const VERSION: &str = "1";
|
||||
const NS: &str = "http://www.ripe.net/rpki/rrdp";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
|
||||
pub struct PublishedObject {
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
content: Bytes,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
hash: Bytes
|
||||
}
|
||||
|
||||
impl PublishedObject {
|
||||
pub fn new(uri: uri::Rsync, content: Bytes) -> Self {
|
||||
let hash = sha256(&content);
|
||||
PublishedObject { uri , content, hash }
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &uri::Rsync{ &self.uri }
|
||||
pub fn content(&self) -> &Bytes { &self.content }
|
||||
pub fn hash(&self) -> &Bytes { &self.hash }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Snapshot {
|
||||
session: String,
|
||||
serial: usize,
|
||||
objects: HashMap<String, Vec<PublishedObject>>
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn new(
|
||||
session: String,
|
||||
serial: usize,
|
||||
objects: HashMap<String, Vec<PublishedObject>>
|
||||
) -> Self {
|
||||
Snapshot { session, serial, objects }
|
||||
}
|
||||
|
||||
pub fn objects(&self) -> &HashMap<String, Vec<PublishedObject>> {
|
||||
&self.objects
|
||||
}
|
||||
|
||||
pub fn to_xml(&self) -> Vec<u8> {
|
||||
XmlWriter::encode_vec(|w| {
|
||||
let a = [
|
||||
("xmlns", NS),
|
||||
("version", VERSION),
|
||||
("session_id", self.session.as_ref()),
|
||||
("serial", &format!("{}", self.serial)),
|
||||
];
|
||||
|
||||
w.put_element(
|
||||
"snapshot",
|
||||
Some(&a),
|
||||
|w| {
|
||||
for uri in self.objects.keys() {
|
||||
let objects = &self.objects[uri];
|
||||
for cf in objects {
|
||||
let uri = cf.uri.to_string();
|
||||
let a = [ ("uri", uri.as_ref()) ];
|
||||
w.put_element(
|
||||
"publish",
|
||||
Some(&a),
|
||||
|w| {
|
||||
w.put_blob(&cf.content)
|
||||
}
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------ Notification --------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Notification {
|
||||
session_id: String,
|
||||
serial: usize,
|
||||
snapshot: SnapshotRef,
|
||||
deltas: Vec<DeltaRef>
|
||||
}
|
||||
|
||||
/// # Accessors
|
||||
///
|
||||
impl Notification {
|
||||
pub fn serial(&self) -> usize {
|
||||
self.serial
|
||||
}
|
||||
pub fn session_id(&self) -> &String { &self.session_id }
|
||||
|
||||
pub fn deltas(&self) -> &Vec<DeltaRef> {
|
||||
&self.deltas
|
||||
}
|
||||
}
|
||||
|
||||
/// # Load and save
|
||||
///
|
||||
impl Notification {
|
||||
|
||||
/// Build up the current notification based on what's on disk.
|
||||
/// Note that this will return None, if there is nothing on disk.
|
||||
///
|
||||
/// Also note that in future we may want to cache things for
|
||||
/// efficiency (but make it work with multi master).
|
||||
pub fn build(
|
||||
path: &PathBuf,
|
||||
base_uri: &uri::Http,
|
||||
rrdp_base: &PathBuf
|
||||
) -> Option<Notification> {
|
||||
let mut builder = NotificationBuilder::new();
|
||||
|
||||
match XmlReader::open(path, |r| -> Result<(), Error> {
|
||||
r.take_named_element("notification", |mut a, r| {
|
||||
{
|
||||
// process attributes
|
||||
builder.with_session_id(a.take_req("session_id")?);
|
||||
let serial = usize::from_str(a.take_req("serial")?.as_ref())?;
|
||||
builder.with_serial(serial);
|
||||
// about NS
|
||||
}
|
||||
|
||||
{
|
||||
// expect snapshot ref
|
||||
r.take_named_element(
|
||||
"snapshot",
|
||||
|mut a, _r| -> Result<(), Error> {
|
||||
let uri = uri::Http::from_string(a.take_req("uri")?)?;
|
||||
let file_info = FileInfo::for_uri(
|
||||
&uri,
|
||||
base_uri,
|
||||
rrdp_base
|
||||
)?;
|
||||
|
||||
builder.with_snapshot(
|
||||
SnapshotRef { file_info }
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
{
|
||||
// deltas
|
||||
loop {
|
||||
let d = r.take_opt_element(|t, mut a, _r| {
|
||||
match t.name.as_ref() {
|
||||
"delta" => {
|
||||
let uri = uri::Http::from_string(
|
||||
a.take_req("uri")?
|
||||
)?;
|
||||
let serial = usize::from_str(
|
||||
a.take_req("serial")?.as_ref()
|
||||
)?;
|
||||
let file_info = FileInfo::for_uri(
|
||||
&uri,
|
||||
base_uri,
|
||||
rrdp_base
|
||||
)?;
|
||||
|
||||
Ok(Some(DeltaRef {
|
||||
serial,
|
||||
file_info
|
||||
}))
|
||||
},
|
||||
_ => Err(Error::NotificationFileError)
|
||||
}
|
||||
})?;
|
||||
match d {
|
||||
None => break,
|
||||
Some(d) => builder.add_delta(d)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}).map_err(|_| Error::NotificationFileError) {
|
||||
Ok(_) => Some(builder.build()),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves a notification file as RFC8182 XML.
|
||||
pub fn save(&self, path: &PathBuf) -> Result<(), Error> {
|
||||
debug!("Writing notification file: {}", path.to_string_lossy());
|
||||
let mut file = file::create_file_with_path(&path)?;
|
||||
|
||||
XmlWriter::encode_to_file(& mut file, |w| {
|
||||
|
||||
let a = [
|
||||
("xmlns", NS),
|
||||
("version", VERSION),
|
||||
("session_id", self.session_id.as_ref()),
|
||||
("serial", &format!("{}", self.serial)),
|
||||
];
|
||||
|
||||
w.put_element(
|
||||
"notification",
|
||||
Some(&a),
|
||||
|w| {
|
||||
{
|
||||
// snapshot ref
|
||||
let uri = self.snapshot.uri.to_string();
|
||||
let hash = &self.snapshot.hash;
|
||||
let a = [
|
||||
("uri", uri.as_str()),
|
||||
("hash", hash)
|
||||
];
|
||||
w.put_element(
|
||||
"snapshot",
|
||||
Some(&a),
|
||||
|w| { w.empty() }
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
// delta refs
|
||||
for delta in &self.deltas {
|
||||
let serial = format!("{}", delta.serial);
|
||||
let uri = delta.uri.to_string();
|
||||
let hash = &delta.hash;
|
||||
let a = [
|
||||
("serial", serial.as_ref()),
|
||||
("uri", uri.as_str()),
|
||||
("hash", hash)
|
||||
];
|
||||
w.put_element(
|
||||
"delta",
|
||||
Some(&a),
|
||||
|w| { w.empty() }
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ SnapshotRef ---------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SnapshotRef {
|
||||
file_info: FileInfo
|
||||
}
|
||||
|
||||
impl SnapshotRef {
|
||||
pub fn new(file_info: FileInfo) -> Self {
|
||||
SnapshotRef { file_info }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SnapshotRef {
|
||||
type Target = FileInfo;
|
||||
|
||||
fn deref(&self) -> &FileInfo {
|
||||
&self.file_info
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ DeltaRef ------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DeltaRef {
|
||||
serial: usize,
|
||||
file_info: FileInfo,
|
||||
}
|
||||
|
||||
impl DeltaRef {
|
||||
pub fn new(serial: usize, file_info: FileInfo) -> Self {
|
||||
DeltaRef { serial, file_info }
|
||||
}
|
||||
pub fn serial(&self) -> &usize {
|
||||
&self.serial
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Deref for DeltaRef {
|
||||
type Target = FileInfo;
|
||||
|
||||
fn deref(&self) -> &FileInfo {
|
||||
&self.file_info
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ FileInfo ------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct FileInfo {
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_http_uri",
|
||||
serialize_with = "ext_serde::ser_http_uri")]
|
||||
uri: uri::Http,
|
||||
hash: String,
|
||||
size: usize
|
||||
}
|
||||
|
||||
impl FileInfo {
|
||||
fn new(uri: uri::Http, hash: String, size: usize) -> FileInfo {
|
||||
FileInfo { uri, hash, size}
|
||||
}
|
||||
|
||||
pub fn for_path_and_uri(
|
||||
path: &PathBuf,
|
||||
uri: uri::Http
|
||||
) -> Result<FileInfo, Error> {
|
||||
let bytes = {
|
||||
use std::io::Read;
|
||||
|
||||
let mut f = File::open(path)?;
|
||||
let mut bytes = Vec::new();
|
||||
f.read_to_end(&mut bytes)?;
|
||||
bytes
|
||||
};
|
||||
|
||||
let size = bytes.len();
|
||||
|
||||
let hash = {
|
||||
use crate::util::sha256;
|
||||
use bytes::Bytes;
|
||||
|
||||
hex::encode(&sha256(&Bytes::from(bytes)))
|
||||
};
|
||||
|
||||
Ok(FileInfo::new(uri, hash, size))
|
||||
}
|
||||
|
||||
pub fn for_uri(
|
||||
uri: &uri::Http,
|
||||
base_uri: &uri::Http,
|
||||
rrdp_base: &PathBuf
|
||||
) -> Result<FileInfo, Error> {
|
||||
let base_string = base_uri.to_string();
|
||||
let uri_string = uri.to_string();
|
||||
|
||||
if ! uri_string.as_str().starts_with(base_string.as_str()) {
|
||||
Err(Error::NotificationFileError)
|
||||
} else {
|
||||
let (_, rel) = uri_string.split_at(base_string.len());
|
||||
let mut path = rrdp_base.clone();
|
||||
path.push(rel);
|
||||
|
||||
FileInfo::for_path_and_uri(&path, uri.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_path(
|
||||
path: &PathBuf,
|
||||
base_uri: &uri::Http,
|
||||
rrdp_base: &PathBuf
|
||||
) -> Result<FileInfo, Error> {
|
||||
let relative = path.strip_prefix(rrdp_base)
|
||||
.map_err(|_| Error::UriConfigError)?.to_string_lossy();
|
||||
let base_uri = base_uri.to_string();
|
||||
let uri = uri::Http::from_string(
|
||||
format!("{}{}", base_uri, relative)
|
||||
).map_err(|_| Error::UriConfigError)?;
|
||||
|
||||
FileInfo::for_path_and_uri(path, uri)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//------------ NotificationBuilder -------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NotificationBuilder {
|
||||
serial: Option<usize>,
|
||||
session_id: Option<String>,
|
||||
snapshot: Option<SnapshotRef>,
|
||||
deltas: Vec<DeltaRef>
|
||||
}
|
||||
|
||||
impl NotificationBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_serial(&mut self, serial: usize) {
|
||||
self.serial = Some(serial);
|
||||
}
|
||||
|
||||
pub fn with_session_id(&mut self, session_id: String) {
|
||||
self.session_id = Some(session_id);
|
||||
}
|
||||
|
||||
pub fn with_snapshot(&mut self, snapshot: SnapshotRef) {
|
||||
self.snapshot = Some(snapshot);
|
||||
}
|
||||
|
||||
pub fn with_deltas(&mut self, deltas: Vec<DeltaRef>) {
|
||||
self.deltas = deltas;
|
||||
}
|
||||
|
||||
pub fn add_delta(&mut self, delta: DeltaRef) {
|
||||
self.deltas.push(delta);
|
||||
}
|
||||
|
||||
pub fn add_delta_to_start(&mut self, delta: DeltaRef) {
|
||||
self.deltas.insert(0, delta);
|
||||
}
|
||||
|
||||
/// Keeps at least two deltas, and beyond that only if the size is
|
||||
/// smaller than the snapshot.
|
||||
///
|
||||
/// Note we may add something to exclude old deltas later, if we find
|
||||
/// that e.g. access to old deltas is very infrequent and excluding
|
||||
/// them would shrink the notification file size.
|
||||
fn curate_deltas(&mut self) {
|
||||
let size_snapshot = match &self.snapshot {
|
||||
Some(snapshot) => snapshot.size,
|
||||
None => 0
|
||||
};
|
||||
let mut total_deltas = 0;
|
||||
let mut count = 0;
|
||||
|
||||
self.deltas.retain(|d| {
|
||||
count += 1;
|
||||
total_deltas += d.size;
|
||||
count <= 2 || total_deltas < size_snapshot
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Builds the notification, panics if any of the options are not set.
|
||||
/// This can only happen if there is a bug.
|
||||
pub fn build(mut self) -> Notification {
|
||||
self.curate_deltas();
|
||||
Notification {
|
||||
serial: self.serial.unwrap(),
|
||||
session_id: self.session_id.unwrap(),
|
||||
snapshot: self.snapshot.unwrap(),
|
||||
deltas: self.deltas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum Error {
|
||||
#[display(fmt="{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
RecursorError(RecursorError),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
UriError(uri::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
XmlReaderErr(XmlReaderErr),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
AttributesError(AttributesError),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
ParseIntError(ParseIntError),
|
||||
|
||||
#[display(fmt="Error with notification file.")]
|
||||
NotificationFileError,
|
||||
|
||||
#[display(fmt="Error with uri config.")]
|
||||
UriConfigError
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Error::IoError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecursorError> for Error {
|
||||
fn from(e: RecursorError) -> Self {
|
||||
Error::RecursorError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uri::Error> for Error {
|
||||
fn from(e: uri::Error) -> Self { Error::UriError(e) }
|
||||
}
|
||||
|
||||
impl From<XmlReaderErr> for Error {
|
||||
fn from(e: XmlReaderErr) -> Self {
|
||||
Error::XmlReaderErr(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AttributesError> for Error {
|
||||
fn from(e: AttributesError) -> Self {
|
||||
Error::AttributesError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for Error {
|
||||
fn from(e: ParseIntError) -> Self { Error::ParseIntError(e) }
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
//! Example implementation using the eventsourcing module.
|
||||
//!
|
||||
//! Goal is two-fold: document using a simple domain, and test the module.
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
|
||||
//------------ InitPersonEvent -----------------------------------------------
|
||||
|
||||
/// Every aggregate defines their own initialisation event. This is the first
|
||||
/// event stored for an instance.
|
||||
///
|
||||
/// Here we define a type wrapping around the generic StoredEvent, so we only
|
||||
/// need to define the unique initialisation details.
|
||||
type InitPersonEvent = StoredEvent<InitPersonDetails>;
|
||||
|
||||
impl InitPersonEvent {
|
||||
pub fn init(id: &AggregateId, name: &str) -> Self {
|
||||
StoredEvent::new(id, 0, InitPersonDetails { name: name.to_string()})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct InitPersonDetails {
|
||||
pub name: String
|
||||
}
|
||||
|
||||
|
||||
//------------ InitPersonEvent -----------------------------------------------
|
||||
|
||||
/// Every aggregate defines their own set of events - i.e. state changes. The
|
||||
/// state of an aggregate can only change when events are applied. And events
|
||||
/// cannot have side effects. If they did, then replaying events would become
|
||||
/// problematic.
|
||||
///
|
||||
/// Here we make a type alias wrapped around the generic StoredEvent and
|
||||
/// include an enum with event details specific for Persons. Furthermore we
|
||||
/// provide an implementation for this type alias so that we can have some
|
||||
/// convenience functions for creating these events.
|
||||
type PersonEvent = StoredEvent<PersonEventDetails>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
enum PersonEventDetails {
|
||||
NameChanged(String),
|
||||
HadBirthday
|
||||
}
|
||||
|
||||
impl PersonEvent {
|
||||
pub fn had_birthday(p: &Person) -> Self {
|
||||
StoredEvent::new(p.id(), p.version, PersonEventDetails::HadBirthday)
|
||||
}
|
||||
|
||||
pub fn name_changed(p: &Person, name: String) -> Self {
|
||||
StoredEvent::new(
|
||||
p.id(),
|
||||
p.version,
|
||||
PersonEventDetails::NameChanged(name))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ PersonCommand -------------------------------------------------
|
||||
|
||||
/// In order to change an aggregate a command is sent to it. The aggregate
|
||||
/// will then validate the command and if there are no issues, it will return
|
||||
/// a list (vec) of events that may be applied. This process in itself does
|
||||
/// not change any state, the state of the aggregate is only changed when
|
||||
/// those events are applied.
|
||||
///
|
||||
/// Commands are not recorded. Only the resulting events are. For this reason
|
||||
/// commands may have side-effects: e.g. write something to disk, send an
|
||||
/// email, etc.
|
||||
///
|
||||
/// Here we define a type wrapping around the generic SentCommand, so we only
|
||||
/// need to provide an enum with specific command details. We also have an
|
||||
/// implementation for this type alias providing some convenience methods.
|
||||
type PersonCommand = SentCommand<PersonCommandDetails>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
enum PersonCommandDetails {
|
||||
ChangeName(String),
|
||||
GoAroundTheSun
|
||||
}
|
||||
|
||||
impl CommandDetails for PersonCommandDetails {
|
||||
type Event = PersonEvent;
|
||||
}
|
||||
|
||||
impl PersonCommand {
|
||||
|
||||
pub fn go_around_sun(id: &AggregateId, version: Option<u64>) -> Self {
|
||||
Self::new(id, version, PersonCommandDetails::GoAroundTheSun)
|
||||
}
|
||||
|
||||
pub fn change_name(id: &AggregateId, version: Option<u64>, s: &str) -> Self {
|
||||
let details = PersonCommandDetails::ChangeName(s.to_string());
|
||||
Self::new(id, version, details)
|
||||
}
|
||||
}
|
||||
|
||||
//------------ PersonError ---------------------------------------------------
|
||||
|
||||
/// Errors specific to the Person aggregate, should only ever be returned when
|
||||
/// applying a command that does not validate.
|
||||
#[derive(Clone, Debug, Display)]
|
||||
enum PersonError {
|
||||
#[display(fmt = "No person can live longer than 255 years")]
|
||||
TooOld
|
||||
}
|
||||
|
||||
impl std::error::Error for PersonError {}
|
||||
|
||||
|
||||
//------------ PersonResult --------------------------------------------------
|
||||
|
||||
/// A shorthand for the result type returned by the process_command function
|
||||
/// of the Person aggregate.
|
||||
type PersonResult = Result<Vec<PersonEvent>, PersonError>;
|
||||
|
||||
|
||||
//------------ Person ------------------------------------------------------
|
||||
|
||||
/// Defines a person object. Persons have a name and an age.
|
||||
///
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct Person {
|
||||
/// The id is needed when generating events.
|
||||
id: AggregateId,
|
||||
|
||||
/// The version of for this particular Person. Versions
|
||||
/// are incremented whenever events are applied. They are
|
||||
/// used to store those and apply events in the correct
|
||||
/// sequence, as well as to detect concurrency issues when
|
||||
/// a command is sent.
|
||||
version: u64,
|
||||
|
||||
name: String,
|
||||
age: u8
|
||||
}
|
||||
|
||||
impl Person {
|
||||
pub fn id(&self) -> &AggregateId { &self.id }
|
||||
pub fn version(&self) -> u64 { self.version }
|
||||
pub fn name(&self) -> &String { &self.name }
|
||||
pub fn age(&self) -> u8 { self.age }
|
||||
}
|
||||
|
||||
impl Aggregate for Person {
|
||||
type Command = PersonCommand;
|
||||
type Event = PersonEvent;
|
||||
type InitEvent = InitPersonEvent;
|
||||
type Error = PersonError;
|
||||
|
||||
fn init(event: InitPersonEvent) -> Result<Self, PersonError> {
|
||||
let (id, _version, init) = event.unwrap();
|
||||
Ok(Person {
|
||||
id, version: 1, name: init.name, age: 0
|
||||
})
|
||||
}
|
||||
|
||||
fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: PersonEvent) {
|
||||
match event.into_details() {
|
||||
PersonEventDetails::NameChanged(name) => { self.name = name },
|
||||
PersonEventDetails::HadBirthday => { self.age += 1 }
|
||||
}
|
||||
self.version += 1;
|
||||
}
|
||||
|
||||
fn process_command(&self, command: Self::Command) -> PersonResult {
|
||||
match command.into_details() {
|
||||
PersonCommandDetails::ChangeName(name) => {
|
||||
let event = PersonEvent::name_changed(&self, name);
|
||||
Ok(vec![event])
|
||||
},
|
||||
PersonCommandDetails::GoAroundTheSun => {
|
||||
if self.age == 255 {
|
||||
Err(PersonError::TooOld)
|
||||
} else {
|
||||
let event = PersonEvent::had_birthday(&self);
|
||||
Ok(vec![event])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// This type is responsible for managing all persons. I.e. creating new
|
||||
/// instances, returning a reference for reading, dispatching commands, and
|
||||
/// storing them.
|
||||
///
|
||||
/// It is generic over the keystore used.
|
||||
///
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
struct PersonManager<S: KeyStore> {
|
||||
/// Here we use a cache to make matters complicated^H interesting..
|
||||
/// Of course this may not always be the best idea..
|
||||
cache: RwLock<HashMap<AggregateId, Arc<Person>>>,
|
||||
|
||||
/// The keystore where snapshots and events may be retrieved and stored.
|
||||
store: S
|
||||
}
|
||||
|
||||
impl<S: KeyStore> PersonManager<S> {
|
||||
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
pub fn new(store: S) -> Self {
|
||||
let values = RwLock::new(HashMap::new());
|
||||
PersonManager {
|
||||
cache: values, store
|
||||
}
|
||||
}
|
||||
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
fn update_cache(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
mut force: bool
|
||||
) -> Result<(), PersonManagerError> {
|
||||
|
||||
let mut cache = self.cache.write().unwrap();
|
||||
|
||||
let mut has_key = cache.contains_key(id);
|
||||
|
||||
if ! has_key {
|
||||
let init_key = S::key_for_event(0);
|
||||
if let Some(init) = self.store.get(id, &init_key)? {
|
||||
let mut agg = Person::init(init)?;
|
||||
|
||||
cache.insert(id.clone(), Arc::new(agg));
|
||||
force = true;
|
||||
has_key = true;
|
||||
}
|
||||
}
|
||||
|
||||
if has_key && force {
|
||||
// We MUST have an entry now
|
||||
let arc = cache.get_mut(id).unwrap();
|
||||
let agg = Arc::make_mut(arc);
|
||||
|
||||
loop {
|
||||
let ver = agg.version();
|
||||
let key = S::key_for_event(ver);
|
||||
if let Some(event) = self.store.get(id, &key)? {
|
||||
agg.apply(event)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Get a reference to the latest version of the aggregate.
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
fn get_latest(
|
||||
&self,
|
||||
id: &AggregateId
|
||||
) -> Result<Option<Arc<Person>>, PersonManagerError> {
|
||||
self.update_cache(id, false)?;
|
||||
Ok(self.cache.read().unwrap().get(id).cloned())
|
||||
}
|
||||
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
fn create(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
event: InitPersonEvent
|
||||
) -> Result<(), PersonManagerError> {
|
||||
self.update_cache(id, true)?;
|
||||
|
||||
let mut cache = self.cache.write().unwrap();
|
||||
if cache.contains_key(id) {
|
||||
Err(PersonManagerError::AggregateAlreadyExists)
|
||||
} else {
|
||||
let key = S::key_for_event(0);
|
||||
self.store.store(id, &key, &event)?;
|
||||
|
||||
let agg = Person::init(event)?;
|
||||
|
||||
cache.insert(id.clone(), Arc::new(agg));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a command to the latest aggregate, save the events and return
|
||||
/// the updated aggregate.
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
fn apply(
|
||||
&self,
|
||||
command: PersonCommand
|
||||
) -> Result<(), PersonManagerError> {
|
||||
let id = command.id().clone();
|
||||
self.update_cache(&id, true)?;
|
||||
|
||||
let mut cache = self.cache.write().unwrap();
|
||||
|
||||
match cache.get_mut(&id) {
|
||||
None => Err(PersonManagerError::AggregateDoesNotExist),
|
||||
Some(agg) => {
|
||||
|
||||
let agg = Arc::make_mut(agg);
|
||||
|
||||
if let Some(version) = command.version() {
|
||||
if version != agg.version() {
|
||||
// TODO check conflicts
|
||||
return Err(PersonManagerError::ConcurrentModification)
|
||||
}
|
||||
}
|
||||
|
||||
let events = agg.process_command(command)?;
|
||||
|
||||
for e in events {
|
||||
let key = S::key_for_event(e.version());
|
||||
self.store.store(&id, &key, &e)?;
|
||||
|
||||
agg.apply(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ PersonManagerError --------------------------------------------
|
||||
|
||||
// Compiler does not see this is used in test
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Display)]
|
||||
enum PersonManagerError {
|
||||
#[display(fmt = "Aggregate does not exist")]
|
||||
AggregateDoesNotExist,
|
||||
|
||||
#[display(fmt = "Aggregate already exists")]
|
||||
AggregateAlreadyExists,
|
||||
|
||||
#[display(fmt = "Concurrent modification. Command rejected")]
|
||||
ConcurrentModification,
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
PersonError(PersonError),
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
KeyStoreError(KeyStoreError),
|
||||
}
|
||||
|
||||
impl From<PersonError> for PersonManagerError {
|
||||
fn from(e: PersonError) -> Self { PersonManagerError::PersonError(e) }
|
||||
}
|
||||
|
||||
impl From<KeyStoreError> for PersonManagerError {
|
||||
fn from(e: KeyStoreError) -> Self { PersonManagerError::KeyStoreError(e) }
|
||||
}
|
||||
|
||||
//------------ Tests ---------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::util::test;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
test::test_with_tmp_dir(|d| {
|
||||
|
||||
let storage = DiskKeyStore::new(d.clone());
|
||||
let manager = PersonManager::new(storage);
|
||||
|
||||
let id_alice = AggregateId::from("alice");
|
||||
let alice_init = InitPersonEvent::init(&id_alice, "alice smith");
|
||||
|
||||
manager.create(&id_alice, alice_init).unwrap();
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith");
|
||||
assert_eq!(alice.age(), 0);
|
||||
|
||||
let mut age = 0;
|
||||
loop {
|
||||
manager.apply(
|
||||
PersonCommand::go_around_sun(&id_alice, None)
|
||||
).unwrap();
|
||||
age = age + 1;
|
||||
if age == 21 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith");
|
||||
assert_eq!(alice.age(), 21);
|
||||
|
||||
manager.apply(
|
||||
PersonCommand::change_name(&id_alice, Some(22), "alice smith-doe")
|
||||
).unwrap();
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith-doe");
|
||||
assert_eq!(alice.age(), 21);
|
||||
|
||||
// Should read state from disk
|
||||
let storage = DiskKeyStore::new(d);
|
||||
let manager = PersonManager::new(storage);
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith-doe");
|
||||
assert_eq!(alice.age(), 21);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
//! Event sourcing support for Krill
|
||||
|
||||
mod es_example; // Example implementation and tests.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json;
|
||||
use crate::util::file;
|
||||
|
||||
|
||||
//------------ Storable ------------------------------------------------------
|
||||
|
||||
pub trait Storable: Clone + Serialize + DeserializeOwned + Sized {}
|
||||
impl<T: Clone + Serialize + DeserializeOwned + Sized> Storable for T { }
|
||||
|
||||
|
||||
//------------ AggregateId ---------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct AggregateId(String);
|
||||
|
||||
impl From<&str> for AggregateId {
|
||||
fn from(s: &str) -> Self {
|
||||
AggregateId(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AggregateId {
|
||||
fn from(s: String) -> Self {
|
||||
AggregateId(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AggregateId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AggregateId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
//------------ Aggregate -----------------------------------------------------
|
||||
|
||||
pub trait Aggregate: Storable {
|
||||
|
||||
type Command: Command<Event = Self::Event>;
|
||||
type Event: Event;
|
||||
type InitEvent: Event;
|
||||
type Error: std::error::Error;
|
||||
|
||||
/// Creates a new instance. Expects an event with data needed to
|
||||
/// initialise the instance. Typically this means that a specific
|
||||
/// 'create' event is passed, with all the needed data, or just an empty
|
||||
/// marker if no data is needed. Implementations must return an error in
|
||||
/// case the instance cannot be created.
|
||||
fn init(event: Self::InitEvent) -> Result<Self, Self::Error>;
|
||||
|
||||
/// Returns the current version of the aggregate.
|
||||
fn version(&self) -> u64;
|
||||
|
||||
/// Moves this, and applies the event to this. This MUST not result in
|
||||
/// any errors. Applying the event just updates data and is side-effect
|
||||
/// free.
|
||||
///
|
||||
/// Note that both self and the event are moved. This is done because we
|
||||
/// want to enable moving data into the new aggregate without the need for
|
||||
/// additional allocations.
|
||||
fn apply(&mut self, event: Self::Event);
|
||||
|
||||
/// Processes a command. I.e. validate the command, and return a list of
|
||||
/// events that will result in the desired new state, but do not apply
|
||||
/// these event here. The command processing must be side-effect free.
|
||||
///
|
||||
/// The command is moved, because we want to enable moving its data
|
||||
/// without reallocating.
|
||||
fn process_command(&self, command: Self::Command) -> Result<Vec<Self::Event>, Self::Error>;
|
||||
}
|
||||
|
||||
|
||||
//------------ Event --------------------------------------------------------
|
||||
|
||||
pub trait Event: Storable {
|
||||
/// Identifies the aggregate, useful when storing and retrieving the event.
|
||||
fn id(&self) -> &AggregateId;
|
||||
|
||||
/// The version of the aggregate that this event updates.
|
||||
/// In other words, an aggregate that is currently at version x, will get
|
||||
/// version x + 1, when the event for version x is applied.
|
||||
fn version(&self) -> u64;
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct StoredEvent<E: Storable> {
|
||||
id: AggregateId,
|
||||
version: u64,
|
||||
#[serde(deserialize_with = "E::deserialize")]
|
||||
details: E
|
||||
}
|
||||
|
||||
impl<E: Storable> StoredEvent<E> {
|
||||
pub fn new(id: &AggregateId, version: u64, event: E) -> Self {
|
||||
StoredEvent { id: id.clone(), version, details: event }
|
||||
}
|
||||
|
||||
pub fn details(&self) -> &E { & self.details }
|
||||
|
||||
pub fn into_details(self) -> E { self.details }
|
||||
|
||||
/// Return the parts of this event.
|
||||
pub fn unwrap(self) -> (AggregateId, u64, E) {
|
||||
(self.id, self.version, self.details)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Storable> Event for StoredEvent<E> {
|
||||
fn id(&self) -> &AggregateId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
|
||||
//------------ Command -------------------------------------------------------
|
||||
|
||||
/// Commands are used to send an intent to change an aggregate.
|
||||
///
|
||||
/// Think of this as the data container for your update API, plus some
|
||||
/// meta-data to ensure that the command is sent to the right instance of an
|
||||
/// Aggregate, and that concurrency issues are handled.
|
||||
pub trait Command: Storable {
|
||||
/// Identify the type of event returned by the aggregate that uses this
|
||||
/// command. This is needed because we may need to check whether a
|
||||
/// command conflicts with recent events.
|
||||
type Event: Event;
|
||||
|
||||
/// Identifies the aggregate, useful when storing and retrieving the event.
|
||||
fn id(&self) -> &AggregateId;
|
||||
|
||||
/// The version of the aggregate that this command updates. If this
|
||||
/// command should update whatever the latest version happens to be, then
|
||||
/// use None here.
|
||||
fn version(&self) -> Option<u64>;
|
||||
|
||||
/// In case of concurrent processing of commands, the aggregate may be
|
||||
/// outdated when a command is applied. In such cases this method expects
|
||||
/// the list of events that happened since the ['affected_version'] and
|
||||
/// will return whether there is a conflict. If there is no conflict that
|
||||
/// the command may be applied again.
|
||||
///
|
||||
/// Note that this defaults to true, which is the safe choice when in
|
||||
/// doubt. If you choose to implement this, then you will also need to
|
||||
/// implement the ['set_affected_version'] function.
|
||||
fn conflicts(&self, _events: &[Self::Event]) -> bool { true }
|
||||
}
|
||||
|
||||
|
||||
//------------ SentCommand ---------------------------------------------------
|
||||
|
||||
/// Convenience wrapper so that implementations can just implement
|
||||
/// ['CommandDetails'] and leave the id and version boilerplate.
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct SentCommand<C: CommandDetails> {
|
||||
id: AggregateId,
|
||||
version: Option<u64>,
|
||||
#[serde(deserialize_with = "C::deserialize")]
|
||||
details: C
|
||||
}
|
||||
|
||||
impl<C: CommandDetails> Command for SentCommand<C> {
|
||||
type Event = C::Event;
|
||||
|
||||
fn id(&self) -> &AggregateId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn version(&self) -> Option<u64> {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CommandDetails> SentCommand<C> {
|
||||
pub fn new(id: &AggregateId, version: Option<u64>, details: C) -> Self {
|
||||
SentCommand { id: id.clone(), version, details }
|
||||
}
|
||||
|
||||
pub fn into_details(self) -> C { self.details }
|
||||
}
|
||||
|
||||
|
||||
//------------ CommandDetails ------------------------------------------------
|
||||
|
||||
/// Implement this for an enum with CommandDetails, so you you can reuse the
|
||||
/// id and version boilerplate from ['SentCommand'].
|
||||
pub trait CommandDetails: Storable {
|
||||
type Event: Event;
|
||||
}
|
||||
|
||||
|
||||
//------------ KeyStore ------------------------------------------------------
|
||||
|
||||
/// Generic KeyStore for AggregateManager
|
||||
pub trait KeyStore {
|
||||
|
||||
type Key;
|
||||
|
||||
fn key_for_snapshot() -> Self::Key;
|
||||
fn key_for_event(version: u64) -> Self::Key;
|
||||
|
||||
/// Returns whether a key already exists.
|
||||
fn has_key(&self, id: &AggregateId, key: &Self::Key) -> bool;
|
||||
|
||||
fn has_aggregate(&self, id: &AggregateId) -> bool;
|
||||
|
||||
fn aggregates(&self) -> Vec<AggregateId>; // Use Iterator?
|
||||
|
||||
/// Throws an error if the key already exists.
|
||||
fn store<V: Any + Serialize>(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
key: &Self::Key,
|
||||
value: &V
|
||||
) -> Result<(), KeyStoreError>;
|
||||
|
||||
/// Get the value for this key, if any exists.
|
||||
fn get<V: Any + Storable>(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
key: &Self::Key
|
||||
) -> Result<Option<V>, KeyStoreError>;
|
||||
}
|
||||
|
||||
|
||||
//------------ KeyStoreError -------------------------------------------------
|
||||
|
||||
/// This type defines possible Errors for KeyStore
|
||||
#[derive(Debug, Display)]
|
||||
pub enum KeyStoreError {
|
||||
#[display(fmt = "{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
JsonError(serde_json::Error),
|
||||
|
||||
#[display(fmt = "Key already exists: {}", _0)]
|
||||
KeyExists(String)
|
||||
}
|
||||
|
||||
impl From<io::Error> for KeyStoreError {
|
||||
fn from(e: io::Error) -> Self { KeyStoreError::IoError(e) }
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for KeyStoreError {
|
||||
fn from(e: serde_json::Error) -> Self { KeyStoreError::JsonError(e) }
|
||||
}
|
||||
|
||||
impl std::error::Error for KeyStoreError { }
|
||||
|
||||
|
||||
//------------ DiskKeyStore --------------------------------------------------
|
||||
|
||||
/// This type can store and retrieve values to/from disk, using json
|
||||
/// serialization.
|
||||
pub struct DiskKeyStore {
|
||||
dir: PathBuf
|
||||
}
|
||||
|
||||
impl KeyStore for DiskKeyStore {
|
||||
type Key = PathBuf;
|
||||
|
||||
fn key_for_snapshot() -> Self::Key {
|
||||
PathBuf::from("snapshot.json")
|
||||
}
|
||||
|
||||
fn key_for_event(version: u64) -> Self::Key {
|
||||
PathBuf::from(format!("delta-{}.json", version))
|
||||
}
|
||||
|
||||
fn has_key(&self, id: &AggregateId, key: &Self::Key) -> bool {
|
||||
self.file_path(id, key).exists()
|
||||
}
|
||||
|
||||
fn has_aggregate(&self, _id: &AggregateId) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn aggregates(&self) -> Vec<AggregateId> {
|
||||
let mut res: Vec<AggregateId> = Vec::new();
|
||||
for d in fs::read_dir(&self.dir).unwrap() {
|
||||
let full_path = d.unwrap().path();
|
||||
let path = full_path.file_name().unwrap();
|
||||
res.push(AggregateId::from(path.to_string_lossy().as_ref()));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn store<V: Any + Serialize>(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
key: &Self::Key,
|
||||
value: &V
|
||||
) -> Result<(), KeyStoreError> {
|
||||
if self.has_key(id, key) {
|
||||
Err(KeyStoreError::KeyExists(key.to_string_lossy().to_string()))
|
||||
} else {
|
||||
let mut f = file::create_file_with_path(&self.file_path(id, key))?;
|
||||
let json = serde_json::to_string(value)?;
|
||||
f.write_all(json.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get<V: Any + Storable>(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
key: &Self::Key
|
||||
) -> Result<Option<V>, KeyStoreError> {
|
||||
if self.has_key(id, key) {
|
||||
let f = File::open(self.file_path(id, key))?;
|
||||
let v: V = serde_json::from_reader(f)?;
|
||||
Ok(Some(v))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiskKeyStore {
|
||||
pub fn new(dir: PathBuf) -> Self {
|
||||
DiskKeyStore { dir }
|
||||
}
|
||||
|
||||
pub fn under_work_dir(
|
||||
work_dir: &PathBuf,
|
||||
name_space: &str
|
||||
) -> Result<Self, io::Error> {
|
||||
let mut path = work_dir.clone();
|
||||
path.push(name_space);
|
||||
if ! path.is_dir() {
|
||||
fs::create_dir_all(&path)?;
|
||||
}
|
||||
Ok(Self::new(path))
|
||||
}
|
||||
|
||||
fn file_path(&self, id: &AggregateId, key: &<Self as KeyStore>::Key) -> PathBuf {
|
||||
let mut file_path = self.dir.clone();
|
||||
file_path.push(id.to_string());
|
||||
file_path.push(key);
|
||||
file_path
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -190,7 +190,9 @@ pub struct CmsAuthData {
|
||||
/// /api/v1/publishers/{handle}
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PublisherDetails {
|
||||
publisher_handle: String,
|
||||
handle: String,
|
||||
|
||||
retired: bool,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
@@ -204,8 +206,8 @@ pub struct PublisherDetails {
|
||||
}
|
||||
|
||||
impl PublisherDetails {
|
||||
pub fn publisher_handle(&self) -> &str {
|
||||
&self.publisher_handle
|
||||
pub fn handle(&self) -> &str {
|
||||
&self.handle
|
||||
}
|
||||
pub fn identity_cert(&self) -> Option<&IdCert> {
|
||||
match self.cms_auth {
|
||||
@@ -213,6 +215,7 @@ impl PublisherDetails {
|
||||
Some(ref details) => Some(&details.id_cert)
|
||||
}
|
||||
}
|
||||
pub fn retired(&self) -> bool { self.retired }
|
||||
}
|
||||
|
||||
impl PartialEq for PublisherDetails {
|
||||
@@ -237,7 +240,7 @@ impl Report for PublisherDetails {
|
||||
let mut res = String::new();
|
||||
|
||||
res.push_str("handle: ");
|
||||
res.push_str(self.publisher_handle.as_str());
|
||||
res.push_str(self.handle.as_str());
|
||||
res.push_str("\n");
|
||||
|
||||
res.push_str("base uri: ");
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ pub mod options;
|
||||
use std::io;
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::api::publishers::Publisher;
|
||||
use crate::api::publisher_data::PublisherRequest;
|
||||
use crate::krillc::data::{
|
||||
ApiResponse,
|
||||
PublisherDetails,
|
||||
@@ -77,7 +77,7 @@ impl KrillClient {
|
||||
Ok(ApiResponse::PublisherList(list))
|
||||
},
|
||||
PublishersCommand::Add(add) => {
|
||||
let pbl = Publisher::new(
|
||||
let pbl = PublisherRequest::new(
|
||||
add.handle,
|
||||
add.token,
|
||||
add.base_uri,
|
||||
@@ -149,7 +149,7 @@ impl KrillClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_publisher(&self, pbl: Publisher) -> Result<ApiResponse, Error> {
|
||||
fn add_publisher(&self, pbl: PublisherRequest) -> Result<ApiResponse, Error> {
|
||||
httpclient::post_json(
|
||||
&self.resolve_uri("api/v1/publishers"),
|
||||
pbl,
|
||||
|
||||
+12
-13
@@ -1,39 +1,38 @@
|
||||
//! Authorization for the API
|
||||
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||
use actix_web::{HttpResponse, HttpRequest, Result};
|
||||
use actix_web::{Form, HttpResponse, HttpRequest, Result};
|
||||
use actix_web::http::HeaderMap;
|
||||
use actix_web::middleware::{Middleware, Started};
|
||||
use actix_web::middleware::identity::RequestIdentity;
|
||||
|
||||
use crate::krilld::krillserver::KrillServer;
|
||||
use actix_web::Form;
|
||||
use crate::eventsourcing::DiskKeyStore;
|
||||
|
||||
|
||||
const ADMIN_API_PATH: &str = "/api/";
|
||||
const PUBLICATION_API_PATH: &str = "/publication/";
|
||||
|
||||
pub struct CheckAuthorisation;
|
||||
|
||||
impl Middleware<Arc<RwLock<KrillServer>>> for CheckAuthorisation {
|
||||
impl Middleware<Arc<RwLock<KrillServer<DiskKeyStore>>>> for CheckAuthorisation {
|
||||
fn start(
|
||||
&self,
|
||||
req: &HttpRequest<Arc<RwLock<KrillServer>>>
|
||||
req: &HttpRequest<Arc<RwLock<KrillServer<DiskKeyStore>>>>
|
||||
) -> Result<Started> {
|
||||
if req.identity() == Some("admin".to_string()) {
|
||||
return Ok(Started::Done)
|
||||
}
|
||||
|
||||
let server: RwLockReadGuard<KrillServer> = req.state().read().unwrap();
|
||||
let server: RwLockReadGuard<KrillServer<DiskKeyStore>> = req.state().read().unwrap();
|
||||
|
||||
let mut allowed = true;
|
||||
|
||||
let token_opt = Self::extract_token(req.headers());
|
||||
|
||||
if req.path().starts_with(ADMIN_API_PATH) {
|
||||
allowed = server.allow_api(token_opt)
|
||||
allowed = server.is_api_allowed(token_opt)
|
||||
} else if req.path().starts_with(PUBLICATION_API_PATH) {
|
||||
let handle_opt = Self::extract_publication_handle(req.path());
|
||||
allowed = server.allow_publication_api(handle_opt, token_opt);
|
||||
allowed = server.is_publication_api_allowed(handle_opt, token_opt);
|
||||
}
|
||||
|
||||
if allowed {
|
||||
@@ -90,7 +89,7 @@ impl Authorizer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_allowed(&self, token_opt: Option<String>) -> bool {
|
||||
pub fn is_api_allowed(&self, token_opt: Option<String>) -> bool {
|
||||
match token_opt {
|
||||
None => false,
|
||||
Some(secret) => self.krill_auth_token == secret
|
||||
@@ -105,11 +104,11 @@ pub struct Login {
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn login_page(
|
||||
req: HttpRequest<Arc<RwLock<KrillServer>>>,
|
||||
req: HttpRequest<Arc<RwLock<KrillServer<DiskKeyStore>>>>,
|
||||
form: Form<Login>
|
||||
) -> HttpResponse {
|
||||
let server: RwLockReadGuard<KrillServer> = req.state().read().unwrap();
|
||||
if server.allow_api(Some(form.token.clone())) {
|
||||
let server: RwLockReadGuard<KrillServer<DiskKeyStore>> = req.state().read().unwrap();
|
||||
if server.is_api_allowed(Some(form.token.clone())) {
|
||||
req.remember("admin".to_string());
|
||||
HttpResponse::Found().header("location", "/api/v1/publishers").finish()
|
||||
} else {
|
||||
|
||||
+134
-66
@@ -1,26 +1,30 @@
|
||||
//! Process requests received, delegate, and wrap up the responses.
|
||||
use std::error;
|
||||
use std::sync::{RwLockReadGuard, RwLockWriteGuard};
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use actix_web::http::StatusCode;
|
||||
use serde::Serialize;
|
||||
use crate::api::publishers;
|
||||
use crate::api::publication;
|
||||
use crate::krilld::http::server::{HttpRequest, PublisherHandle};
|
||||
use crate::api::publisher_data;
|
||||
use crate::api::publisher_data::PublisherHandle;
|
||||
use crate::api::publication_data;
|
||||
use crate::eventsourcing::DiskKeyStore;
|
||||
use crate::krilld::http::server::HttpRequest;
|
||||
use crate::krilld::krillserver::{self, KrillServer};
|
||||
use crate::krilld::pubd;
|
||||
use crate::krilld::pubd::publishers::PublisherError;
|
||||
use crate::krilld::pubd::repo::RrdpServerError;
|
||||
use crate::remote::cmsproxy;
|
||||
use crate::remote::sigmsg::SignedMessage;
|
||||
|
||||
|
||||
//------------ Support Functions ---------------------------------------------
|
||||
|
||||
/// Returns a server in a read lock
|
||||
pub fn ro_server(req: &HttpRequest) -> RwLockReadGuard<KrillServer> {
|
||||
pub fn ro_server(req: &HttpRequest) -> RwLockReadGuard<KrillServer<DiskKeyStore>> {
|
||||
req.state().read().unwrap()
|
||||
}
|
||||
|
||||
/// Returns a server in a write lock
|
||||
pub fn rw_server(req: &HttpRequest) -> RwLockWriteGuard<KrillServer> {
|
||||
pub fn rw_server(req: &HttpRequest) -> RwLockWriteGuard<KrillServer<DiskKeyStore>> {
|
||||
req.state().write().unwrap()
|
||||
}
|
||||
|
||||
@@ -67,7 +71,10 @@ pub fn publishers(req: &HttpRequest) -> HttpResponse {
|
||||
Err(e) => server_error(&Error::ServerError(e)),
|
||||
Ok(publishers) => {
|
||||
render_json(
|
||||
publishers::PublisherList::from(&publishers, "/api/v1/publishers")
|
||||
publisher_data::PublisherList::build(
|
||||
&publishers,
|
||||
"/api/v1/publishers"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -78,7 +85,7 @@ pub fn publishers(req: &HttpRequest) -> HttpResponse {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn add_publisher(
|
||||
req: HttpRequest,
|
||||
pbl: publishers::Publisher
|
||||
pbl: publisher_data::PublisherRequest
|
||||
) -> HttpResponse {
|
||||
let mut server = rw_server(&req);
|
||||
match server.add_publisher(pbl) {
|
||||
@@ -94,10 +101,8 @@ pub fn remove_publisher(
|
||||
req: HttpRequest,
|
||||
handle: PublisherHandle
|
||||
) -> HttpResponse {
|
||||
match rw_server(&req).remove_publisher(handle) {
|
||||
match rw_server(&req).remove_publisher(&handle) {
|
||||
Ok(()) => api_ok(),
|
||||
Err(krillserver::Error::PublisherStore(
|
||||
pubd::Error::UnknownPublisher(_))) => api_ok(),
|
||||
Err(e) => server_error(&Error::ServerError(e))
|
||||
}
|
||||
}
|
||||
@@ -109,11 +114,11 @@ pub fn publisher_details(
|
||||
handle: PublisherHandle
|
||||
) -> HttpResponse {
|
||||
let server = ro_server(&req);
|
||||
match server.publisher(handle) {
|
||||
match server.publisher(&handle) {
|
||||
Ok(None) => api_not_found(),
|
||||
Ok(Some(publisher)) => {
|
||||
render_json(
|
||||
publishers::PublisherDetails::from(
|
||||
publisher_data::PublisherDetails::from(
|
||||
&publisher,
|
||||
"/api/v1/publishers",
|
||||
server.service_base_uri())
|
||||
@@ -130,19 +135,14 @@ pub fn repository_response(
|
||||
req: HttpRequest,
|
||||
handle: PublisherHandle
|
||||
) -> HttpResponse {
|
||||
match ro_server(&req).repository_response(handle) {
|
||||
match ro_server(&req).repository_response(&handle) {
|
||||
Ok(res) => {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/xml")
|
||||
.body(res.encode_vec())
|
||||
},
|
||||
Err(krillserver::Error::PublisherStore
|
||||
(pubd::Error::UnknownPublisher(_))) => {
|
||||
api_not_found()
|
||||
},
|
||||
Err(e) => {
|
||||
server_error(&Error::ServerError(e))
|
||||
}
|
||||
|
||||
Err(e) => server_error(&Error::ServerError(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +156,8 @@ pub fn handle_rfc8181_request(
|
||||
msg: SignedMessage,
|
||||
handle: PublisherHandle
|
||||
) -> HttpResponse {
|
||||
let mut server: RwLockWriteGuard<KrillServer> = rw_server(&req);
|
||||
match server.handle_rfc8181_request(&msg, handle.as_ref()) {
|
||||
let mut server: RwLockWriteGuard<KrillServer<DiskKeyStore>> = rw_server(&req);
|
||||
match server.handle_rfc8181_request(&msg, &handle) {
|
||||
Ok(captured) => {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("application/rpki-publication")
|
||||
@@ -173,10 +173,10 @@ pub fn handle_rfc8181_request(
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn handle_delta(
|
||||
req: HttpRequest,
|
||||
delta: publication::PublishDelta,
|
||||
delta: publication_data::PublishDelta,
|
||||
handle: PublisherHandle
|
||||
) -> HttpResponse {
|
||||
match rw_server(&req).handle_delta(delta, handle.as_ref()) {
|
||||
match rw_server(&req).handle_delta(delta, &handle) {
|
||||
Ok(()) => api_ok(),
|
||||
Err(e) => server_error(&Error::ServerError(e))
|
||||
}
|
||||
@@ -188,7 +188,7 @@ pub fn handle_list(
|
||||
req: HttpRequest,
|
||||
handle: PublisherHandle
|
||||
) -> HttpResponse {
|
||||
match ro_server(&req).handle_list(handle.as_ref()) {
|
||||
match ro_server(&req).handle_list(&handle) {
|
||||
Ok(list) => render_json(list),
|
||||
Err(e) => server_error(&Error::ServerError(e))
|
||||
}
|
||||
@@ -208,6 +208,7 @@ pub fn current_snapshot_json(req: &HttpRequest) -> HttpResponse {
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Error {
|
||||
#[display(fmt = "{}", _0)]
|
||||
ServerError(krillserver::Error),
|
||||
@@ -229,7 +230,7 @@ trait ErrorToCode {
|
||||
fn code(&self) -> usize;
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"Error happened"
|
||||
}
|
||||
@@ -240,17 +241,7 @@ impl ErrorToStatus for Error {
|
||||
match self {
|
||||
Error::ServerError(e) => e.status(),
|
||||
Error::JsonError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::PublisherRequestError => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for Error {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
Error::ServerError(e) => e.code(),
|
||||
Error::JsonError(_) => 1001,
|
||||
Error::PublisherRequestError => 1002,
|
||||
Error::PublisherRequestError => StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,33 +249,20 @@ impl ErrorToCode for Error {
|
||||
impl ErrorToStatus for krillserver::Error {
|
||||
fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
krillserver::Error::CmsProxy(_) => StatusCode::BAD_REQUEST,
|
||||
krillserver::Error::PublisherStore(e) => e.status(),
|
||||
krillserver::Error::Repository(_) => StatusCode::BAD_REQUEST,
|
||||
krillserver::Error::NoIdCert => StatusCode::FORBIDDEN,
|
||||
krillserver::Error::CmsProxy(e) => e.status(),
|
||||
krillserver::Error::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
krillserver::Error::PubServer(e) => e.status()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for krillserver::Error {
|
||||
fn code(&self) -> usize {
|
||||
impl ErrorToStatus for cmsproxy::Error {
|
||||
fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
krillserver::Error::PublisherStore(e) => e.code(),
|
||||
krillserver::Error::Repository(_) => 3002,
|
||||
krillserver::Error::CmsProxy(_) => 3003,
|
||||
krillserver::Error::NoIdCert => 2001,
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for pubd::Error {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
pubd::Error::ForwardSlashInHandle(_) => 1004,
|
||||
pubd::Error::DuplicatePublisher(_) => 1005,
|
||||
pubd::Error::UnknownPublisher(_) => 1006,
|
||||
_ => 3001
|
||||
cmsproxy::Error::ValidationError(_) => StatusCode::FORBIDDEN,
|
||||
cmsproxy::Error::MessageError(_) => StatusCode::BAD_REQUEST,
|
||||
cmsproxy::Error::ResponderError(_) => StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,17 +270,107 @@ impl ErrorToCode for pubd::Error {
|
||||
impl ErrorToStatus for pubd::Error {
|
||||
fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
pubd::Error::ForwardSlashInHandle(_) =>
|
||||
StatusCode::BAD_REQUEST,
|
||||
pubd::Error::DuplicatePublisher(_) =>
|
||||
StatusCode::BAD_REQUEST,
|
||||
pubd::Error::UnknownPublisher(_) =>
|
||||
StatusCode::BAD_REQUEST,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR
|
||||
pubd::Error::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
pubd::Error::InvalidBaseUri => StatusCode::BAD_REQUEST,
|
||||
pubd::Error::InvalidHandle(_) => StatusCode::BAD_REQUEST,
|
||||
pubd::Error::ReservedName(_) => StatusCode::BAD_REQUEST,
|
||||
pubd::Error::DuplicatePublisher(_) => StatusCode::BAD_REQUEST,
|
||||
pubd::Error::UnknownPublisher(_) => StatusCode::FORBIDDEN,
|
||||
pubd::Error::ConcurrentModification(_, _) => StatusCode::BAD_REQUEST,
|
||||
pubd::Error::PublisherError(e) => e.status(),
|
||||
pubd::Error::RrdpServerError(e) => e.status(),
|
||||
pubd::Error::KeyStoreError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl ErrorToStatus for PublisherError {
|
||||
fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
PublisherError::Deactivated => StatusCode::FORBIDDEN,
|
||||
PublisherError::VerificationError(_) => StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToStatus for RrdpServerError {
|
||||
fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
RrdpServerError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl ErrorToCode for Error {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
Error::ServerError(e) => e.code(),
|
||||
Error::JsonError(_) => 1001,
|
||||
Error::PublisherRequestError => 1002
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for krillserver::Error {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
krillserver::Error::NoIdCert => 2001,
|
||||
krillserver::Error::CmsProxy(e) => e.code(),
|
||||
krillserver::Error::IoError(_) => 3001,
|
||||
krillserver::Error::PubServer(e) => e.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for cmsproxy::Error {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
cmsproxy::Error::ValidationError(_) => 2001,
|
||||
cmsproxy::Error::MessageError(_) => 1003,
|
||||
cmsproxy::Error::ResponderError(_) => 3003
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for pubd::Error {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
pubd::Error::IoError(_) => 3001,
|
||||
pubd::Error::InvalidBaseUri => 2002,
|
||||
pubd::Error::InvalidHandle(_) => 1004,
|
||||
pubd::Error::ReservedName(_) => 1007,
|
||||
pubd::Error::DuplicatePublisher(_) => 1005,
|
||||
pubd::Error::UnknownPublisher(_) => 1006,
|
||||
pubd::Error::ConcurrentModification(_, _) => 2003,
|
||||
pubd::Error::PublisherError(e) => e.code(),
|
||||
pubd::Error::RrdpServerError(e) => e.code(),
|
||||
pubd::Error::KeyStoreError(_) => 3001,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for PublisherError {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
PublisherError::Deactivated => 2004,
|
||||
PublisherError::VerificationError(_) => 2005,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToCode for RrdpServerError {
|
||||
fn code(&self) -> usize {
|
||||
match self {
|
||||
RrdpServerError::IoError(_) => 3001
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorResponse {
|
||||
code: usize,
|
||||
|
||||
+59
-41
@@ -3,19 +3,24 @@
|
||||
//! Here we deal with booting and setup, and once active deal with parsing
|
||||
//! arguments and routing of requests, typically handing off to the
|
||||
//! daemon::api::endpoints functions for processing and responding.
|
||||
use std::error;
|
||||
use std::io;
|
||||
use std::fs::File;
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||
use actix_web::{pred, fs, server};
|
||||
use actix_web::{App, FromRequest, HttpResponse};
|
||||
use actix_web::dev::MessageBody;
|
||||
use actix_web::middleware;
|
||||
use actix_web::middleware::identity::CookieIdentityPolicy;
|
||||
use actix_web::middleware::identity::IdentityService;
|
||||
use actix_web::http::{Method, StatusCode};
|
||||
use bcder::decode;
|
||||
use futures::Future;
|
||||
use openssl::ssl::{SslMethod, SslAcceptor, SslAcceptorBuilder, SslFiletype};
|
||||
use crate::api::publication;
|
||||
use crate::api::publishers;
|
||||
use crate::api::publication_data;
|
||||
use crate::api::publisher_data;
|
||||
use crate::api::publisher_data::PublisherHandle;
|
||||
use crate::eventsourcing::DiskKeyStore;
|
||||
use crate::krilld::auth;
|
||||
use crate::krilld::auth::{Authorizer, CheckAuthorisation};
|
||||
use crate::krilld::config::Config;
|
||||
use crate::krilld::endpoints;
|
||||
@@ -24,22 +29,20 @@ use crate::krilld::krillserver;
|
||||
use crate::krilld::krillserver::KrillServer;
|
||||
use crate::remote::rfc8183;
|
||||
use crate::remote::sigmsg::SignedMessage;
|
||||
use actix_web::middleware::identity::IdentityService;
|
||||
use actix_web::middleware::identity::CookieIdentityPolicy;
|
||||
use krilld::auth;
|
||||
|
||||
const NOT_FOUND: &[u8] = include_bytes!("../../../ui/dev/html/404.html");
|
||||
const LOGIN: &[u8] = include_bytes!("../../../ui/dev/html/login.html");
|
||||
|
||||
|
||||
//------------ PubServerApp --------------------------------------------------
|
||||
|
||||
pub struct PubServerApp(App<Arc<RwLock<KrillServer>>>);
|
||||
pub struct PubServerApp(App<Arc<RwLock<KrillServer<DiskKeyStore>>>>);
|
||||
|
||||
|
||||
/// # Set up methods
|
||||
///
|
||||
impl PubServerApp {
|
||||
pub fn new(server: Arc<RwLock<KrillServer>>) -> Self {
|
||||
pub fn new(server: Arc<RwLock<KrillServer<DiskKeyStore>>>) -> Self {
|
||||
let mut app = App::with_state(server)
|
||||
.middleware(middleware::Logger::default())
|
||||
.middleware(IdentityService::new(
|
||||
@@ -106,29 +109,36 @@ impl PubServerApp {
|
||||
PubServerApp(with_statics(app))
|
||||
}
|
||||
|
||||
pub fn create_server(config: &Config) -> Arc<RwLock<KrillServer>> {
|
||||
pub fn create_server(
|
||||
config: &Config
|
||||
) -> Result<Arc<RwLock<KrillServer<DiskKeyStore>>>, Error> {
|
||||
let authorizer = Authorizer::new(&config.auth_token);
|
||||
let pub_server = match KrillServer::build(
|
||||
|
||||
let pubserver_store = DiskKeyStore::under_work_dir(&config.data_dir, "pubsrv")?;
|
||||
|
||||
let pub_server = KrillServer::build(
|
||||
&config.data_dir,
|
||||
&config.rsync_base,
|
||||
config.service_uri(),
|
||||
&config.rrdp_base_uri,
|
||||
authorizer
|
||||
) {
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
::std::process::exit(1);
|
||||
},
|
||||
Ok(server) => server
|
||||
};
|
||||
Arc::new(RwLock::new(pub_server))
|
||||
authorizer,
|
||||
pubserver_store
|
||||
)?;
|
||||
|
||||
Ok(Arc::new(RwLock::new(pub_server)))
|
||||
}
|
||||
|
||||
/// Used to start the server with an existing executor (for tests)
|
||||
///
|
||||
/// Note https is not supported in tests.
|
||||
pub fn start(config: &Config) {
|
||||
let ps = PubServerApp::create_server(config);
|
||||
let ps = match PubServerApp::create_server(config) {
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
server::new(move || PubServerApp::new(ps.clone()))
|
||||
.bind(config.socket_addr())
|
||||
@@ -139,7 +149,13 @@ impl PubServerApp {
|
||||
|
||||
/// Used to run the server in blocking mode, from the main method.
|
||||
pub fn run(config: &Config) {
|
||||
let ps = PubServerApp::create_server(config);
|
||||
let ps = match PubServerApp::create_server(config) {
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let server = server::new(move || PubServerApp::new(ps.clone()));
|
||||
|
||||
@@ -214,7 +230,8 @@ impl PubServerApp {
|
||||
// https://github.com/actix/actix-website/blob/master/content/docs/static-files.md
|
||||
// https://www.keycdn.com/blog/http-cache-headers
|
||||
fn serve_rrdp_files(req: &HttpRequest) -> HttpResponse {
|
||||
let server: RwLockReadGuard<KrillServer> = req.state().read().unwrap();
|
||||
let server: RwLockReadGuard<KrillServer<DiskKeyStore>> = req.state().read()
|
||||
.unwrap();
|
||||
|
||||
match req.match_info().get("path") {
|
||||
Some(path) => {
|
||||
@@ -277,7 +294,7 @@ impl<S: 'static> FromRequest<S> for SignedMessage {
|
||||
/// PublisherRequestChoice, which contains either an
|
||||
/// rfc8183::PublisherRequest, or an API publisher request (no ID certs and
|
||||
/// CMS etc).
|
||||
impl<S: 'static> FromRequest<S> for publishers::Publisher {
|
||||
impl<S: 'static> FromRequest<S> for publisher_data::PublisherRequest {
|
||||
type Config = ();
|
||||
type Result = Box<Future<Item=Self, Error=actix_web::Error>>;
|
||||
|
||||
@@ -288,7 +305,7 @@ impl<S: 'static> FromRequest<S> for publishers::Publisher {
|
||||
Box::new(MessageBody::new(req)
|
||||
.from_err()
|
||||
.and_then(|bytes| {
|
||||
let p: publishers::Publisher =
|
||||
let p: publisher_data::PublisherRequest =
|
||||
serde_json::from_reader(bytes.as_ref())
|
||||
.map_err(Error::JsonError)?;
|
||||
Ok(p)
|
||||
@@ -300,10 +317,6 @@ impl<S: 'static> FromRequest<S> for publishers::Publisher {
|
||||
|
||||
//------------ PublisherHandle -----------------------------------------------
|
||||
|
||||
/// Defines a publisher_handle in a path, then can be built with FromRequest,
|
||||
/// so that we can easily use this as a parameter on server methods.
|
||||
pub struct PublisherHandle(pub String);
|
||||
|
||||
impl<S> FromRequest<S> for PublisherHandle {
|
||||
type Config = ();
|
||||
type Result = Result<Self, actix_web::Error>;
|
||||
@@ -313,24 +326,17 @@ impl<S> FromRequest<S> for PublisherHandle {
|
||||
_cfg: &Self::Config
|
||||
) -> Self::Result {
|
||||
if let Some(handle) = req.match_info().get("handle") {
|
||||
let handle = handle.to_string();
|
||||
Ok(PublisherHandle(handle))
|
||||
Ok(PublisherHandle::from(handle))
|
||||
} else {
|
||||
Err(Error::WrongPath.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PublisherHandle {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ PublishDelta --------------------------------------------------
|
||||
/// Support converting request body into PublishDelta
|
||||
impl<S: 'static> FromRequest<S> for publication::PublishDelta {
|
||||
impl<S: 'static> FromRequest<S> for publication_data::PublishDelta {
|
||||
type Config = ();
|
||||
type Result = Box<Future<Item=Self, Error=actix_web::Error>>;
|
||||
|
||||
@@ -341,7 +347,7 @@ impl<S: 'static> FromRequest<S> for publication::PublishDelta {
|
||||
Box::new(MessageBody::new(req).limit(255 * 1024 * 1024) // up to 256MB
|
||||
.from_err()
|
||||
.and_then(|bytes| {
|
||||
let delta: publication::PublishDelta =
|
||||
let delta: publication_data::PublishDelta =
|
||||
serde_json::from_reader(bytes.as_ref())?;
|
||||
Ok(delta)
|
||||
})
|
||||
@@ -353,7 +359,7 @@ impl<S: 'static> FromRequest<S> for publication::PublishDelta {
|
||||
//------------ IntoHttpHandler -----------------------------------------------
|
||||
|
||||
impl server::IntoHttpHandler for PubServerApp {
|
||||
type Handler = <App<Arc<RwLock<KrillServer>>> as server::IntoHttpHandler>::Handler;
|
||||
type Handler = <App<Arc<RwLock<KrillServer<DiskKeyStore>>>> as server::IntoHttpHandler>::Handler;
|
||||
|
||||
fn into_handler(self) -> Self::Handler {
|
||||
self.0.into_handler()
|
||||
@@ -374,12 +380,13 @@ fn with_statics<S: 'static>(app: App<S>) -> App<S> {
|
||||
|
||||
//------------ HttpRequest ---------------------------------------------------
|
||||
|
||||
pub type HttpRequest = actix_web::HttpRequest<Arc<RwLock<KrillServer>>>;
|
||||
pub type HttpRequest = actix_web::HttpRequest<Arc<RwLock<KrillServer<DiskKeyStore>>>>;
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Error {
|
||||
#[display(fmt = "{}", _0)]
|
||||
ServerError(krillserver::Error),
|
||||
@@ -396,6 +403,9 @@ pub enum Error {
|
||||
#[display(fmt = "Wrong path")]
|
||||
WrongPath,
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
Other(String),
|
||||
}
|
||||
@@ -404,7 +414,15 @@ impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self { Error::JsonError(e) }
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self { Error::IoError(e) }
|
||||
}
|
||||
|
||||
impl From<krillserver::Error> for Error {
|
||||
fn from(e: krillserver::Error) -> Self { Error::ServerError(e) }
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
"An error happened"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::util::file;
|
||||
use crate::util::softsigner::SignerKeyId;
|
||||
use crate::remote::builder;
|
||||
|
||||
|
||||
const KEY_SIZE: u32 = 2048;
|
||||
pub const HTTPS_SUB_DIR: &str = "ssl";
|
||||
pub const KEY_FILE: &str = "key.pem";
|
||||
|
||||
+88
-108
@@ -1,20 +1,21 @@
|
||||
//! An RPKI publication protocol server.
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use bcder::Captured;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::api::publishers;
|
||||
use crate::api::publication_data;
|
||||
use crate::api::publisher_data;
|
||||
use crate::api::publisher_data::PublisherHandle;
|
||||
use crate::eventsourcing::KeyStore;
|
||||
use crate::krilld::auth::Authorizer;
|
||||
use crate::krilld::pubd::{self, PublisherStore};
|
||||
use crate::krilld::pubd::repo::{self, Repository};
|
||||
use crate::krilld::pubd::PubServer;
|
||||
use crate::krilld::pubd;
|
||||
use crate::krilld::pubd::publishers::Publisher;
|
||||
use crate::remote::cmsproxy::{self, CmsProxy};
|
||||
use crate::remote::rfc8183;
|
||||
use crate::remote::sigmsg::SignedMessage;
|
||||
|
||||
/// # Naming things in the keystore.
|
||||
const ACTOR: &str = "krill pubd";
|
||||
|
||||
|
||||
//------------ KrillServer ---------------------------------------------------
|
||||
|
||||
@@ -30,8 +31,7 @@ const ACTOR: &str = "krill pubd";
|
||||
/// * Process publish / list requests by known publishers
|
||||
/// * Updates the repository on disk
|
||||
/// * Updates the RRDP files
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KrillServer {
|
||||
pub struct KrillServer<S: KeyStore> {
|
||||
// The base URI for this service
|
||||
service_uri: uri::Http,
|
||||
|
||||
@@ -45,14 +45,11 @@ pub struct KrillServer {
|
||||
cms_proxy: CmsProxy,
|
||||
|
||||
// The configured publishers
|
||||
publisher_store: PublisherStore,
|
||||
|
||||
// The repository responsible for publishing rsync and rrdp
|
||||
repository: Repository,
|
||||
pubserver: PubServer<S>
|
||||
}
|
||||
|
||||
/// # Set up and initialisation
|
||||
impl KrillServer {
|
||||
impl<S: KeyStore> KrillServer<S> {
|
||||
/// Creates a new publication server. Note that state is preserved
|
||||
/// on disk in the work_dir provided.
|
||||
pub fn build(
|
||||
@@ -60,11 +57,20 @@ impl KrillServer {
|
||||
base_uri: &uri::Rsync,
|
||||
service_uri: uri::Http,
|
||||
rrdp_base_uri: &uri::Http,
|
||||
authorizer: Authorizer
|
||||
authorizer: Authorizer,
|
||||
store: S
|
||||
) -> Result<Self, Error> {
|
||||
let cms_proxy = CmsProxy::build(work_dir)?;
|
||||
let publisher_store = PublisherStore::build(work_dir, base_uri)?;
|
||||
let repository = Repository::build(rrdp_base_uri, work_dir)?;
|
||||
|
||||
let mut repo_dir = work_dir.clone();
|
||||
repo_dir.push("repo");
|
||||
|
||||
let pubserver = PubServer::build(
|
||||
base_uri.clone(),
|
||||
rrdp_base_uri.clone(),
|
||||
repo_dir,
|
||||
store
|
||||
).map_err(Error::PubServer)?;
|
||||
|
||||
Ok(
|
||||
KrillServer {
|
||||
@@ -72,8 +78,7 @@ impl KrillServer {
|
||||
work_dir: work_dir.clone(),
|
||||
authorizer,
|
||||
cms_proxy,
|
||||
publisher_store,
|
||||
repository,
|
||||
pubserver
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -83,22 +88,23 @@ impl KrillServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl KrillServer {
|
||||
pub fn allow_api(&self, token_opt: Option<String>) -> bool {
|
||||
self.authorizer.api_allowed(token_opt)
|
||||
impl<S: KeyStore> KrillServer<S> {
|
||||
pub fn is_api_allowed(&self, token_opt: Option<String>) -> bool {
|
||||
self.authorizer.is_api_allowed(token_opt)
|
||||
}
|
||||
|
||||
pub fn allow_publication_api(
|
||||
pub fn is_publication_api_allowed(
|
||||
&self,
|
||||
handle_opt: Option<String>,
|
||||
token_opt: Option<String>
|
||||
) -> bool {
|
||||
match handle_opt {
|
||||
None => false,
|
||||
Some(handle) => {
|
||||
Some(handle_str) => {
|
||||
match token_opt {
|
||||
None => false,
|
||||
Some(token) => {
|
||||
let handle = PublisherHandle::from(handle_str);
|
||||
if let Ok(Some(pbl)) = self.publisher(&handle) {
|
||||
pbl.token() == &token
|
||||
} else {
|
||||
@@ -113,46 +119,37 @@ impl KrillServer {
|
||||
}
|
||||
|
||||
/// # Configure publishers
|
||||
impl KrillServer {
|
||||
/// Returns all currently configured publishers.
|
||||
pub fn publishers(&self) -> Result<Vec<Arc<publishers::Publisher>>, Error> {
|
||||
self.publisher_store
|
||||
.publishers()
|
||||
.map_err(|e| { Error::PublisherStore(e) })
|
||||
impl<S: KeyStore> KrillServer<S> {
|
||||
|
||||
/// Returns all currently configured publishers. (excludes deactivated)
|
||||
pub fn publishers(
|
||||
&self
|
||||
) -> Result<Vec<PublisherHandle>, Error> {
|
||||
self.pubserver.list_publishers().map_err(Error::PubServer)
|
||||
}
|
||||
|
||||
/// Adds the publishers, blows up if it already existed.
|
||||
pub fn add_publisher(
|
||||
&mut self,
|
||||
pbl: publishers::Publisher
|
||||
pbl_req: publisher_data::PublisherRequest
|
||||
) -> Result<(), Error> {
|
||||
|
||||
self.publisher_store.add_publisher(
|
||||
pbl,
|
||||
ACTOR
|
||||
)?;
|
||||
Ok(())
|
||||
self.pubserver.create_publisher(pbl_req).map_err(Error::PubServer)
|
||||
}
|
||||
|
||||
/// Removes a publisher, blows up if it didn't exist.
|
||||
pub fn remove_publisher(
|
||||
&mut self,
|
||||
name: impl AsRef<str>
|
||||
handle: &PublisherHandle
|
||||
) -> Result<(), Error> {
|
||||
self.publisher_store.remove_publisher(
|
||||
name,
|
||||
ACTOR
|
||||
)?;
|
||||
Ok(())
|
||||
self.pubserver.deactivate_publisher(handle).map_err(Error::PubServer)
|
||||
}
|
||||
|
||||
/// Returns an option for a publisher.
|
||||
pub fn publisher(
|
||||
&self,
|
||||
name: impl AsRef<str>
|
||||
) -> Result<Option<Arc<publishers::Publisher>>, Error> {
|
||||
self.publisher_store.publisher(name)
|
||||
.map_err(Error::PublisherStore)
|
||||
handle: &PublisherHandle
|
||||
) -> Result<Option<Arc<Publisher>>, Error> {
|
||||
self.pubserver.get_publisher(handle).map_err(Error::PubServer)
|
||||
}
|
||||
|
||||
/// Returns a repository response for the given publisher.
|
||||
@@ -160,16 +157,20 @@ impl KrillServer {
|
||||
/// Returns an error if the publisher is unknown.
|
||||
pub fn repository_response(
|
||||
&self,
|
||||
name: impl AsRef<str>
|
||||
handle: &PublisherHandle
|
||||
) -> Result<rfc8183::RepositoryResponse, Error> {
|
||||
let publisher = self.publisher_store.get_publisher(name)?;
|
||||
let rrdp_notification = self.repository.rrdp_notification_uri();
|
||||
self.cms_proxy
|
||||
.repository_response(
|
||||
&publisher,
|
||||
self.service_base_uri(),
|
||||
rrdp_notification)
|
||||
.map_err(Error::CmsProxy)
|
||||
match self.pubserver.get_publisher(handle)? {
|
||||
None => Err(Error::NoIdCert),
|
||||
Some(publisher) => {
|
||||
let rrdp_notify = self.pubserver.rrdp_notification();
|
||||
self.cms_proxy
|
||||
.repository_response(
|
||||
&publisher,
|
||||
self.service_base_uri(),
|
||||
rrdp_notify)
|
||||
.map_err(Error::CmsProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rrdp_base_path(&self) -> PathBuf {
|
||||
@@ -181,7 +182,7 @@ impl KrillServer {
|
||||
|
||||
/// # Handle publication requests
|
||||
///
|
||||
impl KrillServer {
|
||||
impl<S: KeyStore> KrillServer<S> {
|
||||
|
||||
/// Handles an incoming SignedMessage, verifies it's validly signed by
|
||||
/// a known publisher and process the QueryMessage contained. Returns
|
||||
@@ -198,13 +199,17 @@ impl KrillServer {
|
||||
pub fn handle_rfc8181_request(
|
||||
&mut self,
|
||||
sigmsg: &SignedMessage,
|
||||
handle: &str
|
||||
handle: &PublisherHandle
|
||||
) -> Result<Captured, Error> {
|
||||
debug!("Handling request for: {}", handle);
|
||||
let publisher = self.publisher_store.get_publisher(handle)?;
|
||||
debug!("Handling request for: {}", handle.to_string());
|
||||
|
||||
let publisher = match self.pubserver.get_publisher(handle)? {
|
||||
Some(publisher) => publisher,
|
||||
None => return Err(Error::NoIdCert)
|
||||
};
|
||||
|
||||
let id_cert = match publisher.cms_auth_data() {
|
||||
Some(details) => details.id_cert(),
|
||||
Some(data) => data.id_cert(),
|
||||
None => return Err(Error::NoIdCert)
|
||||
};
|
||||
|
||||
@@ -212,13 +217,13 @@ impl KrillServer {
|
||||
Err(e) => self.cms_proxy.wrap_error(&e).map_err(Error::CmsProxy),
|
||||
Ok(req) => {
|
||||
let reply = match req {
|
||||
publication::PublishRequest::List => {
|
||||
publication_data::PublishRequest::List => {
|
||||
self.handle_list(handle)
|
||||
.map(publication::PublishReply::List)
|
||||
.map(publication_data::PublishReply::List)
|
||||
},
|
||||
publication::PublishRequest::Delta(delta) => {
|
||||
publication_data::PublishRequest::Delta(delta) => {
|
||||
self.handle_delta(delta, handle)
|
||||
.map(|_| publication::PublishReply::Success)
|
||||
.map(|_| publication_data::PublishReply::Success)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -226,7 +231,7 @@ impl KrillServer {
|
||||
Ok(reply) => {
|
||||
self.cms_proxy.wrap_publish_reply(reply).map_err(Error::CmsProxy)
|
||||
},
|
||||
Err(Error::Repository(e)) => {
|
||||
Err(Error::PubServer(e)) => {
|
||||
self.cms_proxy.wrap_error(&e).map_err(Error::CmsProxy)
|
||||
},
|
||||
Err(e) => Err(e)
|
||||
@@ -240,75 +245,50 @@ impl KrillServer {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn handle_delta(
|
||||
&mut self,
|
||||
delta: publication::PublishDelta,
|
||||
handle: &str
|
||||
delta: publication_data::PublishDelta,
|
||||
handle: &PublisherHandle
|
||||
) -> Result<(), Error> {
|
||||
let publisher = self.publisher_store.get_publisher(handle)?;
|
||||
let base_uri = publisher.base_uri();
|
||||
self.repository.publish(&delta, base_uri).map_err(Error::Repository)
|
||||
self.pubserver.publish(handle, delta).map_err(Error::PubServer)
|
||||
}
|
||||
|
||||
/// Handles a list request sent to the API, or.. through the CmsProxy.
|
||||
pub fn handle_list(
|
||||
&self,
|
||||
handle: &str
|
||||
) -> Result<publication::ListReply, Error> {
|
||||
let publisher = self.publisher_store.get_publisher(handle)?;
|
||||
let base_uri = publisher.base_uri();
|
||||
self.repository.list(base_uri).map_err(Error::Repository)
|
||||
handle: &PublisherHandle
|
||||
) -> Result<publication_data::ListReply, Error> {
|
||||
self.pubserver.list(handle).map_err(Error::PubServer)
|
||||
}
|
||||
}
|
||||
|
||||
// /// # Serve RRDP files
|
||||
// ///
|
||||
//impl KrillServer {
|
||||
// /// Gets the current notification
|
||||
// pub fn current_notification(&self) -> Result<repo::Notification, Error> {
|
||||
// unimplemented!()
|
||||
// }
|
||||
//
|
||||
// /// Gets the current snapshot
|
||||
// pub fn current_snapshot(&self) -> Result<data::Snapshot, Error> {
|
||||
// unimplemented!()
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Error {
|
||||
#[display(fmt="{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
CmsProxy(cmsproxy::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
PublisherStore(pubd::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
Repository(repo::Error),
|
||||
PubServer(pubd::Error),
|
||||
|
||||
#[display(fmt="No IdCert known for this publisher")]
|
||||
NoIdCert
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self { Error::IoError(e) }
|
||||
}
|
||||
|
||||
impl From<cmsproxy::Error> for Error {
|
||||
fn from(e: cmsproxy::Error) -> Self {
|
||||
Error::CmsProxy(e)
|
||||
}
|
||||
fn from(e: cmsproxy::Error) -> Self { Error::CmsProxy(e) }
|
||||
}
|
||||
|
||||
impl From<pubd::Error> for Error {
|
||||
fn from(e: pubd::Error) -> Self {
|
||||
Error::PublisherStore(e)
|
||||
}
|
||||
fn from(e: pubd::Error) -> Self { Error::PubServer(e) }
|
||||
}
|
||||
|
||||
impl From<repo::Error> for Error {
|
||||
fn from(e: repo::Error) -> Self {
|
||||
Error::Repository(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Tested through integration tests
|
||||
+798
-261
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,266 @@
|
||||
use rpki::uri;
|
||||
use crate::api::publication_data;
|
||||
use crate::api::publisher_data::{
|
||||
CmsAuthData,
|
||||
PublisherHandle,
|
||||
PublisherRequest
|
||||
};
|
||||
use crate::api::repo_data::{
|
||||
CurrentObjects,
|
||||
DeltaElements,
|
||||
VerificationError
|
||||
};
|
||||
use crate::eventsourcing::{
|
||||
Aggregate,
|
||||
CommandDetails,
|
||||
StoredEvent,
|
||||
SentCommand
|
||||
};
|
||||
use crate::util::ext_serde;
|
||||
|
||||
|
||||
//------------ PublisherInit -------------------------------------------------
|
||||
|
||||
pub type PublisherInit = StoredEvent<InitPublisherDetails>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct InitPublisherDetails {
|
||||
token: String,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
base_uri: uri::Rsync,
|
||||
cms_auth_data: Option<CmsAuthData>
|
||||
}
|
||||
|
||||
impl PublisherInit {
|
||||
pub fn init(
|
||||
id: &PublisherHandle,
|
||||
token: String,
|
||||
base_uri: uri::Rsync,
|
||||
cms_auth_data: Option<CmsAuthData>
|
||||
) -> Self {
|
||||
StoredEvent::new(
|
||||
id,
|
||||
0,
|
||||
InitPublisherDetails { token, base_uri, cms_auth_data }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublisherRequest> for PublisherInit {
|
||||
fn from(req: PublisherRequest) -> Self {
|
||||
let (handle, token, base_uri, cms_auth_data) = req.unwrap(); // (self
|
||||
let id = PublisherHandle::from(handle);
|
||||
let details = InitPublisherDetails { token, base_uri, cms_auth_data };
|
||||
StoredEvent::new(&id, 0, details)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ PublisherEvent ------------------------------------------------
|
||||
|
||||
pub type PublisherEvent = StoredEvent<PublisherEventDetails>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub enum PublisherEventDetails {
|
||||
Deactivated,
|
||||
Published(DeltaElements)
|
||||
}
|
||||
|
||||
impl PublisherEvent {
|
||||
pub fn deactivated(id: &PublisherHandle, version: u64) -> Self {
|
||||
PublisherEvent::new(id, version, PublisherEventDetails::Deactivated)
|
||||
}
|
||||
|
||||
pub fn published(
|
||||
id: &PublisherHandle,
|
||||
version: u64,
|
||||
delta: DeltaElements
|
||||
) -> Self {
|
||||
PublisherEvent::new(id, version, PublisherEventDetails::Published(delta))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ PublisherCommand ----------------------------------------------
|
||||
|
||||
pub type PublisherCommand = SentCommand<PublisherCommandDetails>;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub enum PublisherCommandDetails {
|
||||
Deactivate,
|
||||
Publish(publication_data::PublishDelta)
|
||||
}
|
||||
|
||||
impl CommandDetails for PublisherCommandDetails {
|
||||
type Event = PublisherEvent;
|
||||
}
|
||||
|
||||
impl PublisherCommand {
|
||||
pub fn deactivate(id: &PublisherHandle) -> Self {
|
||||
PublisherCommand::new(id, None, PublisherCommandDetails::Deactivate)
|
||||
}
|
||||
|
||||
pub fn publish(id: &PublisherHandle, delta: publication_data::PublishDelta) -> Self {
|
||||
PublisherCommand::new(id, None, PublisherCommandDetails::Publish(delta))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ PublisherError ------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Display)]
|
||||
pub enum PublisherError {
|
||||
#[display(fmt = "Publisher is (already) de-activated")]
|
||||
Deactivated,
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
VerificationError(VerificationError),
|
||||
}
|
||||
|
||||
impl From<VerificationError> for PublisherError {
|
||||
fn from(e: VerificationError) -> Self {
|
||||
PublisherError::VerificationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PublisherError {}
|
||||
|
||||
|
||||
//------------ Publisher -----------------------------------------------------
|
||||
|
||||
/// This type defines Publisher CAs that are allowed to publish.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Publisher {
|
||||
/// Aggregate house keeping
|
||||
id: PublisherHandle,
|
||||
version: u64,
|
||||
retired: bool,
|
||||
|
||||
/// Publication jail for this publisher
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_rsync_uri",
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
base_uri: uri::Rsync,
|
||||
|
||||
/// The token used by the API
|
||||
token: String,
|
||||
|
||||
/// The optional RFC8181 identity, for the RFC8183 pub protocol.
|
||||
cms_auth_data: Option<CmsAuthData>,
|
||||
|
||||
/// All objects currently published by this publisher, by hash
|
||||
current_objects: CurrentObjects
|
||||
}
|
||||
|
||||
/// # Accessors
|
||||
impl Publisher {
|
||||
pub fn id(&self) -> &PublisherHandle {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn retired(&self) -> bool { self.retired }
|
||||
|
||||
pub fn token(&self) -> &String {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn base_uri(&self) -> &uri::Rsync {
|
||||
&self.base_uri
|
||||
}
|
||||
|
||||
pub fn cms_auth_data(&self) -> &Option<CmsAuthData> {
|
||||
&self.cms_auth_data
|
||||
}
|
||||
}
|
||||
|
||||
/// # Life cycle
|
||||
///
|
||||
impl Publisher {
|
||||
|
||||
fn create(event: PublisherInit) -> Self {
|
||||
let (id, _version, init) = event.unwrap();
|
||||
Publisher {
|
||||
id,
|
||||
version: 1,
|
||||
retired: false,
|
||||
token: init.token,
|
||||
base_uri: init.base_uri,
|
||||
cms_auth_data: init.cms_auth_data,
|
||||
current_objects: CurrentObjects::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn deactivate(&self) -> Result<Vec<PublisherEvent>, PublisherError> {
|
||||
if self.retired {
|
||||
Err(PublisherError::Deactivated)
|
||||
} else {
|
||||
let e = PublisherEvent::deactivated(&self.id, self.version);
|
||||
Ok(vec![e])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// # Publication protocol
|
||||
///
|
||||
impl Publisher {
|
||||
/// Gets an owned list reply containing all objects for this publisher.
|
||||
/// Note that cloning the uris and hashes is relatively cheap because of
|
||||
/// the use of Bytes as the underlying structure. Still, it may be good
|
||||
/// to change this implementation in future to return a structure that
|
||||
/// takes references, and only lives long enough to compose a response.
|
||||
pub fn list_current(&self) -> publication_data::ListReply {
|
||||
self.current_objects.to_list_reply()
|
||||
}
|
||||
|
||||
/// Verifies a delta command and returns an event containing the delta,
|
||||
/// provided that it's legitimate.
|
||||
fn process_delta_cmd(
|
||||
&self,
|
||||
delta: publication_data::PublishDelta
|
||||
) -> Result<Vec<PublisherEvent>, PublisherError> {
|
||||
|
||||
let delta = DeltaElements::from(delta);
|
||||
self.current_objects.verify_delta(&delta, &self.base_uri)?;
|
||||
|
||||
Ok(vec![PublisherEvent::published(&self.id, self.version, delta)])
|
||||
}
|
||||
|
||||
fn apply_delta(&mut self, delta: DeltaElements) {
|
||||
self.current_objects.apply_delta(delta);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Aggregate for Publisher {
|
||||
type Command = PublisherCommand;
|
||||
type Event = PublisherEvent;
|
||||
type InitEvent = PublisherInit;
|
||||
type Error = PublisherError;
|
||||
|
||||
fn init(event: Self::InitEvent) -> Result<Self, Self::Error> {
|
||||
Ok(Self::create(event))
|
||||
}
|
||||
|
||||
fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Self::Event) {
|
||||
match event.into_details() {
|
||||
PublisherEventDetails::Deactivated => self.retired = true,
|
||||
PublisherEventDetails::Published(delta) => self.apply_delta(delta)
|
||||
}
|
||||
self.version += 1;
|
||||
}
|
||||
|
||||
fn process_command(&self, command: Self::Command) -> Result<Vec<Self::Event>, Self::Error> {
|
||||
match command.into_details() {
|
||||
PublisherCommandDetails::Deactivate => self.deactivate(),
|
||||
PublisherCommandDetails::Publish(delta) => self.process_delta_cmd(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
+454
-250
@@ -1,266 +1,470 @@
|
||||
use std::{io, fs};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::krilld::pubd::rsyncd;
|
||||
use crate::krilld::pubd::rrdpd;
|
||||
use crate::api::repo_data::{
|
||||
Delta,
|
||||
DeltaElements,
|
||||
DeltaRef,
|
||||
FileRef,
|
||||
Notification,
|
||||
NotificationUpdate,
|
||||
Snapshot,
|
||||
SnapshotRef,
|
||||
};
|
||||
use crate::eventsourcing::{
|
||||
Aggregate,
|
||||
AggregateId,
|
||||
CommandDetails,
|
||||
StoredEvent,
|
||||
SentCommand,
|
||||
};
|
||||
use crate::util::{
|
||||
ext_serde,
|
||||
file,
|
||||
Time
|
||||
};
|
||||
|
||||
//------------ Repository ----------------------------------------------------
|
||||
|
||||
/// This type orchestrates publishing in both an RSYNC and RRDP
|
||||
/// (RFC8182) format.
|
||||
const RRDP_FOLDER: &str = "rrdp";
|
||||
const RSYNC_FOLDER: &str = "rsync";
|
||||
|
||||
// Todo: make a const fn once that is stable.
|
||||
pub fn rrdp_id() -> AggregateId { AggregateId::from("rrdp_server")}
|
||||
|
||||
|
||||
|
||||
//------------ RrdpInit ------------------------------------------------------
|
||||
|
||||
pub type RrdpInit = StoredEvent<RrdpInitDetails>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RrdpInitDetails {
|
||||
session: String,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_http_uri",
|
||||
serialize_with = "ext_serde::ser_http_uri")]
|
||||
base_uri: uri::Http,
|
||||
|
||||
repo_dir: PathBuf
|
||||
}
|
||||
|
||||
impl RrdpInit {
|
||||
pub fn init_new(base_uri: uri::Http, repo_dir: PathBuf) -> Self {
|
||||
use rand::{thread_rng, Rng};
|
||||
let mut rng = thread_rng();
|
||||
let rnd: u32 = rng.gen();
|
||||
let session = format!("{}", rnd);
|
||||
|
||||
StoredEvent::new(
|
||||
&rrdp_id(),
|
||||
0,
|
||||
RrdpInitDetails { session, base_uri, repo_dir }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RrdpEvent ------------------------------------------------------
|
||||
|
||||
pub type RrdpEvent = StoredEvent<RrdpEventDetails>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum RrdpEventDetails {
|
||||
AddedDelta(Delta),
|
||||
UpdatedNotification(NotificationUpdate),
|
||||
CleanedUp(Time)
|
||||
}
|
||||
|
||||
impl RrdpEvent {
|
||||
fn added_delta(id: &AggregateId, ver: u64, delta: Delta) -> Self {
|
||||
StoredEvent::new(id, ver, RrdpEventDetails::AddedDelta(delta))
|
||||
}
|
||||
|
||||
fn updated_notification(
|
||||
id: &AggregateId,
|
||||
ver: u64,
|
||||
notif: NotificationUpdate
|
||||
) -> Self {
|
||||
StoredEvent::new(id, ver, RrdpEventDetails::UpdatedNotification(notif))
|
||||
}
|
||||
|
||||
fn cleaned_up(
|
||||
id: &AggregateId,
|
||||
ver: u64,
|
||||
time: Time
|
||||
) -> Self {
|
||||
StoredEvent::new(id, ver, RrdpEventDetails::CleanedUp(time))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RrdpCommand ---------------------------------------------------
|
||||
|
||||
pub type RrdpCommand = SentCommand<RrdpCommandDetails>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum RrdpCommandDetails {
|
||||
AddDelta(DeltaElements),
|
||||
Publish,
|
||||
Cleanup(RetentionTime)
|
||||
}
|
||||
|
||||
/// The retention time for snapshot and delta files no longer referenced.
|
||||
pub type RetentionTime = Duration;
|
||||
|
||||
impl CommandDetails for RrdpCommandDetails {
|
||||
type Event = RrdpEvent;
|
||||
}
|
||||
|
||||
impl RrdpCommand {
|
||||
pub fn add_delta(delta: DeltaElements) -> Self {
|
||||
SentCommand::new(&rrdp_id(), None, RrdpCommandDetails::AddDelta(delta))
|
||||
}
|
||||
|
||||
pub fn publish() -> Self {
|
||||
SentCommand::new(&rrdp_id(), None, RrdpCommandDetails::Publish)
|
||||
}
|
||||
|
||||
pub fn clean_up(retention: RetentionTime) -> Self {
|
||||
SentCommand::new(&rrdp_id(), None, RrdpCommandDetails::Cleanup(retention))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RrdpServerError -----------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum RrdpServerError {
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
impl From<io::Error> for RrdpServerError {
|
||||
fn from(e: io::Error) -> Self { RrdpServerError::IoError(e) }
|
||||
}
|
||||
|
||||
impl std::error::Error for RrdpServerError {}
|
||||
|
||||
|
||||
//------------ RrdpResult ----------------------------------------------------
|
||||
|
||||
pub type RrdpResult = Result<Vec<RrdpEvent>, RrdpServerError>;
|
||||
|
||||
|
||||
//------------ RrdpServer ----------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RrdpServer {
|
||||
// aggregate ID is fixed using a const str RRDP_ID
|
||||
version: u64,
|
||||
|
||||
session: String,
|
||||
serial: u64,
|
||||
|
||||
/// The base URI for notification, snapshot and delta files.
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_http_uri",
|
||||
serialize_with = "ext_serde::ser_http_uri")]
|
||||
base_uri: uri::Http,
|
||||
|
||||
/// The base directory where notification, snapshot and deltas will be
|
||||
/// published.
|
||||
rrdp_base: PathBuf,
|
||||
|
||||
notification: Notification,
|
||||
snapshot: Snapshot,
|
||||
deltas: Vec<Delta>
|
||||
}
|
||||
|
||||
/// # Publishing
|
||||
///
|
||||
impl RrdpServer {
|
||||
|
||||
fn process_published_delta(&mut self, delta: Delta) {
|
||||
self.snapshot.apply_delta(delta.clone());
|
||||
self.deltas.insert(0, delta);
|
||||
|
||||
// Keep a minimum of 2 deltas, and a maximum for which the combined
|
||||
// number of elements does not exceed the number of elements in the
|
||||
// snapshot.
|
||||
{
|
||||
let size_snapshot = self.snapshot.len();
|
||||
let mut total_deltas = 0;
|
||||
let mut count = 0;
|
||||
|
||||
self.deltas.retain(|d| {
|
||||
count += 1;
|
||||
total_deltas += d.len();
|
||||
count <= 2 || total_deltas < size_snapshot
|
||||
});
|
||||
}
|
||||
|
||||
self.serial += 1;
|
||||
}
|
||||
|
||||
/// Creates a delta for the delta elements. Assumes (for now) that the
|
||||
/// delta elements have been verified for the publisher.
|
||||
fn add_delta(&self, elements: DeltaElements) -> RrdpResult {
|
||||
let next = self.serial + 1;
|
||||
let session = self.session.clone();
|
||||
let delta = Delta::new(session, next, elements);
|
||||
|
||||
Ok(vec![RrdpEvent::added_delta(&rrdp_id(), self.version, delta)])
|
||||
}
|
||||
|
||||
/// Publishes the latest notification, snapshot and delta file to disk.
|
||||
/// Return event to move old files to clean-up list.
|
||||
fn publish(&self) -> RrdpResult {
|
||||
let snapshot_hash = self.snapshot.write_xml(&self.snapshot_path())?;
|
||||
let snapshot_ref = SnapshotRef::new(
|
||||
self.snapshot_uri(),
|
||||
self.snapshot_path(),
|
||||
snapshot_hash
|
||||
);
|
||||
|
||||
// Note we always have at least 1 delta when publishing.
|
||||
let last_delta = &self.deltas[0];
|
||||
let delta_hash = last_delta.write_xml(&self.delta_path(last_delta.serial()))?;
|
||||
let delta_ref = DeltaRef::new(
|
||||
last_delta.serial(),
|
||||
FileRef::new(
|
||||
self.delta_uri(last_delta.serial()),
|
||||
self.delta_path(last_delta.serial()),
|
||||
delta_hash
|
||||
)
|
||||
);
|
||||
|
||||
let update = NotificationUpdate::new(
|
||||
Time::now(),
|
||||
None,
|
||||
snapshot_ref,
|
||||
delta_ref,
|
||||
self.deltas.last().unwrap().serial()
|
||||
);
|
||||
|
||||
let mut notification = self.notification.clone();
|
||||
notification.update(update.clone());
|
||||
notification.write_xml(&self.notification_path())?;
|
||||
|
||||
Ok(vec![
|
||||
RrdpEvent::updated_notification(
|
||||
&rrdp_id(),
|
||||
self.version,
|
||||
update
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
/// Cleans out old files on disk, returns event for cleaning up the state.
|
||||
fn cleanup(&self, retention: RetentionTime) -> RrdpResult {
|
||||
let cut_off = Time::before_now(retention);
|
||||
for old in self.notification.old_refs() {
|
||||
if old.0.on_or_before(&cut_off) {
|
||||
// Don't care if it were already deleted.
|
||||
let _ = file::clean_file_and_path(&old.1.path());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![
|
||||
RrdpEvent::cleaned_up(
|
||||
&rrdp_id(),
|
||||
self.version,
|
||||
cut_off
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// rrdp paths and uris
|
||||
///
|
||||
impl RrdpServer {
|
||||
pub fn notification_uri(&self) -> uri::Http {
|
||||
uri::Http::from_string(
|
||||
format!("{}notifcation.xml", self.base_uri.to_string())
|
||||
).unwrap() // Cannot fail. Config checked at startup.
|
||||
}
|
||||
|
||||
fn notification_path(&self) -> PathBuf {
|
||||
let mut path = self.rrdp_base.clone();
|
||||
path.push("notification.xml");
|
||||
path
|
||||
}
|
||||
|
||||
fn snapshot_rel(session: &str, serial: u64) -> String {
|
||||
format!("{}/{}/snapshot.xml", session, serial)
|
||||
}
|
||||
|
||||
fn new_snapshot_path(base: &PathBuf, session: &str, serial: u64) -> PathBuf {
|
||||
let mut path = base.clone();
|
||||
path.push(Self::snapshot_rel(session, serial));
|
||||
path
|
||||
}
|
||||
|
||||
fn snapshot_path(&self) -> PathBuf {
|
||||
Self::new_snapshot_path(&self.rrdp_base, &self.session, self.serial)
|
||||
}
|
||||
|
||||
fn new_snapshot_uri(base: &uri::Http, session: &str, serial: u64) -> uri::Http {
|
||||
uri::Http::from_string(
|
||||
format!("{}{}",
|
||||
base.to_string(),
|
||||
Self::snapshot_rel(session, serial)
|
||||
)
|
||||
).unwrap() // Cannot fail. Config checked at startup.
|
||||
}
|
||||
|
||||
fn snapshot_uri(&self) -> uri::Http {
|
||||
Self::new_snapshot_uri(&self.base_uri, &self.session, self.serial)
|
||||
}
|
||||
|
||||
fn delta_rel(session: &str, serial: u64) -> String {
|
||||
format!("{}/{}/delta.xml", session, serial)
|
||||
}
|
||||
|
||||
fn delta_uri(&self, serial: u64) -> uri::Http {
|
||||
uri::Http::from_string(
|
||||
format!("{}{}",
|
||||
self.base_uri.to_string(),
|
||||
Self::delta_rel(&self.session, serial)
|
||||
)
|
||||
).unwrap() // Cannot fail. Config checked at startup.
|
||||
}
|
||||
|
||||
fn delta_path(&self, serial: u64) -> PathBuf {
|
||||
let mut path = self.rrdp_base.clone();
|
||||
path.push(Self::delta_rel(&self.session, serial));
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Aggregate for RrdpServer {
|
||||
type Command = RrdpCommand;
|
||||
type Event = RrdpEvent;
|
||||
type InitEvent = RrdpInit;
|
||||
type Error = RrdpServerError;
|
||||
|
||||
fn init(event: Self::InitEvent) -> Result<Self, Self::Error> {
|
||||
let init = event.into_details();
|
||||
let version = 1;
|
||||
let session = init.session;
|
||||
let base_uri = init.base_uri;
|
||||
let mut rrdp_base = init.repo_dir.clone();
|
||||
rrdp_base.push(RRDP_FOLDER);
|
||||
|
||||
let serial = 0;
|
||||
let snapshot = Snapshot::new(session.clone());
|
||||
|
||||
let snapshot_path = Self::new_snapshot_path(&rrdp_base, &session, 0);
|
||||
let snapshot_uri = Self::new_snapshot_uri(&base_uri, &session, 0);
|
||||
let snapshot_hash = snapshot.write_xml(&snapshot_path)?;
|
||||
|
||||
let snapshot_ref = SnapshotRef::new(
|
||||
snapshot_uri,
|
||||
snapshot_path,
|
||||
snapshot_hash
|
||||
);
|
||||
|
||||
let notification = Notification::create(session.clone(), snapshot_ref);
|
||||
let deltas = vec![];
|
||||
|
||||
Ok(RrdpServer {
|
||||
version,
|
||||
session,
|
||||
base_uri,
|
||||
rrdp_base,
|
||||
serial,
|
||||
notification,
|
||||
snapshot,
|
||||
deltas
|
||||
})
|
||||
}
|
||||
|
||||
fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Self::Event) {
|
||||
match event.into_details() {
|
||||
RrdpEventDetails::AddedDelta(delta) =>
|
||||
self.process_published_delta(delta),
|
||||
RrdpEventDetails::UpdatedNotification(notification) =>
|
||||
self.notification.update(notification),
|
||||
RrdpEventDetails::CleanedUp(time) =>
|
||||
self.notification.clean_up(time)
|
||||
}
|
||||
self.version += 1;
|
||||
|
||||
}
|
||||
|
||||
fn process_command(&self, command: Self::Command) -> RrdpResult {
|
||||
match command.into_details() {
|
||||
RrdpCommandDetails::AddDelta(els) => self.add_delta(els),
|
||||
RrdpCommandDetails::Publish => self.publish(),
|
||||
RrdpCommandDetails::Cleanup(retention) => self.cleanup(retention),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ RsyncdStore ---------------------------------------------------
|
||||
|
||||
/// This type is responsible for publishing files on disk in a structure so
|
||||
/// that an rscynd can be set up to serve this (RPKI) data. Note that the
|
||||
/// rsync host name and module are part of the path, so make sure that the
|
||||
/// rsyncd modules and paths are setup properly for each supported rsync
|
||||
/// base uri used.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Repository {
|
||||
// file_store
|
||||
fs: rsyncd::FileStore,
|
||||
|
||||
// RRDP
|
||||
rrdp: rrdpd::RrdpServer
|
||||
pub struct RsyncdStore {
|
||||
rsync_dir: PathBuf
|
||||
}
|
||||
|
||||
/// # Construct
|
||||
///
|
||||
impl Repository {
|
||||
pub fn build(
|
||||
rrdp_base_uri: &uri::Http,
|
||||
work_dir: &PathBuf
|
||||
) -> Result<Self, Error>
|
||||
{
|
||||
let fs = rsyncd::FileStore::build(work_dir)?;
|
||||
let rrdp = rrdpd::RrdpServer::build(rrdp_base_uri, work_dir)?;
|
||||
Ok( Repository { fs, rrdp } )
|
||||
impl RsyncdStore {
|
||||
pub fn build(repo_dir: &PathBuf) -> Result<Self, io::Error> {
|
||||
let mut rsync_dir = PathBuf::from(repo_dir);
|
||||
rsync_dir.push(RSYNC_FOLDER);
|
||||
if ! rsync_dir.is_dir() {
|
||||
fs::create_dir_all(&rsync_dir)?;
|
||||
}
|
||||
Ok ( RsyncdStore { rsync_dir } )
|
||||
}
|
||||
}
|
||||
|
||||
/// # Access
|
||||
/// # Publishing
|
||||
///
|
||||
impl Repository {
|
||||
/// Returns the RRDP notification URI for inclusion in the
|
||||
/// Repository Response
|
||||
pub fn rrdp_notification_uri(&self) -> uri::Http {
|
||||
self.rrdp.notification_uri().clone()
|
||||
}
|
||||
}
|
||||
impl RsyncdStore {
|
||||
/// Saves all the publishes and updates, deletes all the withdraws.
|
||||
pub fn publish(&self, delta: &DeltaElements) -> Result<(), io::Error> {
|
||||
|
||||
/// # Publish / List
|
||||
///
|
||||
impl Repository {
|
||||
/// Publishes an publish query and returns a success reply embedded in
|
||||
/// a message. Throws an error in case of issues. The PubServer needs
|
||||
/// to wrap such errors in a response message to the publisher.
|
||||
pub fn publish(
|
||||
&mut self,
|
||||
delta: &publication::PublishDelta,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<(), Error> {
|
||||
debug!("Processing update with {} elements", delta.len());
|
||||
self.fs.publish(delta, base_uri)?;
|
||||
self.rrdp.publish(delta, base_uri)?;
|
||||
for p in delta.publishes() {
|
||||
file::save_with_rsync_uri(
|
||||
&p.base64().to_bytes(),
|
||||
&self.rsync_dir,
|
||||
p.uri()
|
||||
)?;
|
||||
}
|
||||
|
||||
for u in delta.updates() {
|
||||
file::save_with_rsync_uri(
|
||||
&u.base64().to_bytes(),
|
||||
&self.rsync_dir,
|
||||
u.uri()
|
||||
)?;
|
||||
}
|
||||
|
||||
for w in delta.withdraws() {
|
||||
file::delete_with_rsync_uri(
|
||||
&self.rsync_dir,
|
||||
w.uri()
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists the objects for a base_uri, presumably all for the same
|
||||
/// publisher.
|
||||
pub fn list(
|
||||
&self,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<publication::ListReply, Error> {
|
||||
debug!("Processing list query");
|
||||
let files = self.fs.list(base_uri)?;
|
||||
Ok(publication::ListReply::from_files(files))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum Error {
|
||||
#[display(fmt="{}", _0)]
|
||||
Rrdpd(rrdpd::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
Rsyncd(rsyncd::Error),
|
||||
}
|
||||
|
||||
impl From<rrdpd::Error> for Error {
|
||||
fn from(e: rrdpd::Error) -> Self { Error::Rrdpd(e) }
|
||||
}
|
||||
|
||||
impl From<rsyncd::Error> for Error {
|
||||
fn from(e: rsyncd::Error) -> Self { Error::Rsyncd(e) }
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------ Tests ---------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use crate::util::file::CurrentFile;
|
||||
use crate::util::test;
|
||||
use crate::api::publication::PublishDeltaBuilder;
|
||||
use crate::api::rrdp_data;
|
||||
|
||||
#[test]
|
||||
fn should_publish() {
|
||||
test::test_with_tmp_dir(|d| {
|
||||
let rrdp_base_uri = test::http_uri("http://localhost:3000/repo/");
|
||||
let mut repo = Repository::build(&rrdp_base_uri, &d).unwrap() ;
|
||||
|
||||
// Publish a file
|
||||
let rsync_for_alice =
|
||||
test::rsync_uri("rsync://host:10873/module/alice");
|
||||
let file = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/alice/file.txt"),
|
||||
Bytes::from("example content")
|
||||
);
|
||||
|
||||
let mut builder = PublishDeltaBuilder::new();
|
||||
builder.add_publish(file.as_publish());
|
||||
let delta = builder.finish();
|
||||
|
||||
repo.publish(&delta, &rsync_for_alice).unwrap();
|
||||
|
||||
// Now publish an update a bunch of times
|
||||
// (overwrite file with same file, strictly speaking allowed,
|
||||
// and convenient here)
|
||||
|
||||
let file_update = file.clone();
|
||||
|
||||
let mut builder = PublishDeltaBuilder::new();
|
||||
builder.add_update(file_update.as_update(file.hash()));
|
||||
let delta = builder.finish();
|
||||
|
||||
repo.publish(&delta, &rsync_for_alice).unwrap();
|
||||
repo.publish(&delta, &rsync_for_alice).unwrap();
|
||||
repo.publish(&delta, &rsync_for_alice).unwrap();
|
||||
repo.publish(&delta, &rsync_for_alice).unwrap();
|
||||
repo.publish(&delta, &rsync_for_alice).unwrap();
|
||||
|
||||
// Now we expect a notification file with serial 6, which only
|
||||
// includes deltas for 5 and 6, because more deltas would
|
||||
// exceed the size of the snapshot.
|
||||
|
||||
let mut rrdp_disk_path = d.clone();
|
||||
rrdp_disk_path.push("rrdp");
|
||||
|
||||
let mut notification_disk_path = rrdp_disk_path.clone();
|
||||
notification_disk_path.push("notification.xml");
|
||||
|
||||
match rrdp_data::Notification::build(
|
||||
¬ification_disk_path,
|
||||
&rrdp_base_uri,
|
||||
&rrdp_disk_path
|
||||
) {
|
||||
Some(notification) => {
|
||||
let expected_serial: usize = 6;
|
||||
let expected_prev: usize = 5;
|
||||
assert_eq!(notification.serial(), expected_serial);
|
||||
|
||||
let deltas = notification.deltas();
|
||||
assert_eq!(2, deltas.len());
|
||||
|
||||
assert!(
|
||||
deltas.iter().find(|d| {
|
||||
d.serial() == &expected_serial}
|
||||
).is_some()
|
||||
);
|
||||
|
||||
assert!(
|
||||
deltas.iter().find(|d| {
|
||||
d.serial() == &expected_prev}
|
||||
).is_some()
|
||||
);
|
||||
},
|
||||
None => panic!("Should have derived notification"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_store_list_withdraw_files() {
|
||||
test::test_with_tmp_dir(|d| {
|
||||
let mut file_store = rsyncd::FileStore::build(&d).unwrap();
|
||||
|
||||
// Using a port here to make sure that it works in mapping
|
||||
// the rsync URI to and from disk.
|
||||
let base_uri = test::rsync_uri("rsync://host:10873/module/alice/");
|
||||
|
||||
// Publish a file
|
||||
let file = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/alice/file.txt"),
|
||||
Bytes::from("example content")
|
||||
);
|
||||
|
||||
let mut builder = PublishDeltaBuilder::new();
|
||||
builder.add_publish(file.as_publish());
|
||||
let delta = builder.finish();
|
||||
|
||||
file_store.publish(&delta, &base_uri).unwrap();
|
||||
|
||||
// See that it's the only one listed
|
||||
let files = file_store.list(&base_uri).unwrap();
|
||||
assert_eq!(1, files.len());
|
||||
assert!(files.contains(&file));
|
||||
|
||||
// Update a file
|
||||
let file_update = CurrentFile::new(
|
||||
file.uri().clone(),
|
||||
Bytes::from("example updated content")
|
||||
);
|
||||
|
||||
let mut builder = PublishDeltaBuilder::new();
|
||||
builder.add_update(file_update.as_update(file.hash()));
|
||||
let delta = builder.finish();
|
||||
file_store.publish(&delta, &base_uri).unwrap();
|
||||
|
||||
// See that it's the only one listed
|
||||
let files = file_store.list(&base_uri).unwrap();
|
||||
assert_eq!(1, files.len());
|
||||
assert!(files.contains(&file_update));
|
||||
|
||||
// Withdraw a file
|
||||
let mut builder = PublishDeltaBuilder::new();
|
||||
builder.add_withdraw(file_update.as_withdraw());
|
||||
let delta = builder.finish();
|
||||
file_store.publish(&delta, &base_uri).unwrap();
|
||||
|
||||
// See that there are no files listed
|
||||
let files = file_store.list(&base_uri).unwrap();
|
||||
assert_eq!(0, files.len());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_allow_publishing_or_withdrawing_outside_of_base() {
|
||||
test::test_with_tmp_dir(|d| {
|
||||
let mut file_store = rsyncd::FileStore::build(&d).unwrap();
|
||||
|
||||
// Using a port here to make sure that it works in mapping
|
||||
// the rsync URI to and from disk.
|
||||
let base_uri = test::rsync_uri("rsync://host:10873/module/alice/");
|
||||
|
||||
// Publish a file
|
||||
let file = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/bob/file.txt"),
|
||||
Bytes::from("example content")
|
||||
);
|
||||
|
||||
let mut builder = PublishDeltaBuilder::new();
|
||||
builder.add_publish(file.as_publish());
|
||||
let delta = builder.finish();
|
||||
|
||||
match file_store.publish(&delta, &base_uri) {
|
||||
Err(rsyncd::Error::OutsideBaseUri) => {},
|
||||
_ => { panic!("Expected Error::OutsideBaseUri") }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,489 +0,0 @@
|
||||
use std::io;
|
||||
use std::fs;
|
||||
use std::num::ParseIntError;
|
||||
use std::path::PathBuf;
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::api::rrdp_data::{self, Notification};
|
||||
use crate::api::rrdp_data::Snapshot;
|
||||
use crate::api::rrdp_data::PublishedObject;
|
||||
use crate::util::file::{self, RecursorError};
|
||||
use crate::storage::keystore::{self, Key, KeyStore};
|
||||
use crate::storage::caching_ks::CachingDiskKeyStore;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use storage::keystore::Info;
|
||||
use api::rrdp_data::FileInfo;
|
||||
use api::rrdp_data::DeltaRef;
|
||||
use util::xml::XmlWriter;
|
||||
use api::rrdp_data::NotificationBuilder;
|
||||
use api::rrdp_data::SnapshotRef;
|
||||
|
||||
//const VERSION: &'static str = "1";
|
||||
//const NS: &'static str = "http://www.ripe.net/rpki/rrdp";
|
||||
const RRDP_FOLDER: &str = "rrdp";
|
||||
const FS_FOLDER: &str = "rsync";
|
||||
|
||||
const VERSION: &str = "1";
|
||||
const NS: &str = "http://www.ripe.net/rpki/rrdp";
|
||||
|
||||
|
||||
|
||||
/// This type publishes RRDP notifications, snapshots and deltas so that they
|
||||
/// can be served to relying parties.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RrdpServer {
|
||||
store: CachingDiskKeyStore,
|
||||
|
||||
// The base URI path for notification, snapshot and delta files.
|
||||
base_uri: uri::Http,
|
||||
|
||||
// Dir for notification, snapshot and delta files.
|
||||
rrdp_base: PathBuf,
|
||||
|
||||
// Dir for file_store (so that snapshots can be derived)
|
||||
fs_base: PathBuf
|
||||
}
|
||||
|
||||
/// # Setup and initialisation
|
||||
impl RrdpServer {
|
||||
|
||||
/// Creates a new RrdpServer.
|
||||
///
|
||||
/// This will pick up the saved state from the notification.xml if
|
||||
/// present, or initialise a new server with a random session_id,
|
||||
/// starting at serial 1, and including a snapshot for everything
|
||||
/// currently stored in the rsync file_store.
|
||||
pub fn build(
|
||||
base_uri: &uri::Http,
|
||||
work_dir: &PathBuf
|
||||
) -> Result<Self, Error>
|
||||
{
|
||||
if ! base_uri.to_string().ends_with('/') {
|
||||
return Err(Error::UriConfigError)
|
||||
}
|
||||
|
||||
let mut rrdp_store_dir = PathBuf::from(work_dir);
|
||||
rrdp_store_dir.push("rrdp_store");
|
||||
if ! rrdp_store_dir.is_dir() {
|
||||
fs::create_dir_all(&rrdp_store_dir)?;
|
||||
}
|
||||
|
||||
let store = CachingDiskKeyStore::build(rrdp_store_dir)?;
|
||||
|
||||
let rrdp_base = file::sub_dir(work_dir, RRDP_FOLDER)?;
|
||||
let fs_base = file::sub_dir(work_dir, FS_FOLDER)?;
|
||||
Ok(RrdpServer { store, base_uri: base_uri.clone(), rrdp_base, fs_base })
|
||||
}
|
||||
}
|
||||
|
||||
/// # Storing, Retrieving, and referencing Notification, Snapshot, Deltas
|
||||
///
|
||||
impl RrdpServer {
|
||||
|
||||
// const REL_NOTIFICATION: &'static str = "notification.xml";
|
||||
|
||||
fn key_notification() -> Key {
|
||||
Key::new("notification")
|
||||
}
|
||||
|
||||
fn key_snapshot(session: &str, serial: usize) -> Key {
|
||||
Key::new(&format!("{}-{}-snapshot", session, serial))
|
||||
}
|
||||
|
||||
pub fn get_notification(
|
||||
&self
|
||||
) -> Result<Option<Arc<Notification>>, Error> {
|
||||
let key = Self::key_notification();
|
||||
self.store.get(&key).map_err(Error::Keystore)
|
||||
}
|
||||
|
||||
pub fn get_snapshot(
|
||||
&self,
|
||||
session: &str,
|
||||
serial: usize
|
||||
) -> Result<Option<Arc<Snapshot>>, Error> {
|
||||
let key = Self::key_snapshot(session, serial);
|
||||
self.store.get(&key).map_err(Error::Keystore )
|
||||
}
|
||||
|
||||
pub fn save_notification(
|
||||
&mut self,
|
||||
notification: Notification
|
||||
) -> Result<(), Error> {
|
||||
let key = Self::key_notification();
|
||||
self.store.store(
|
||||
key,
|
||||
notification,
|
||||
Info::now("server", "notification")
|
||||
).map_err(Error::Keystore)
|
||||
}
|
||||
|
||||
pub fn save_snapshot(
|
||||
&mut self,
|
||||
session: &str,
|
||||
serial: usize,
|
||||
snapshot: Snapshot
|
||||
) -> Result<(), Error> {
|
||||
let key = Self::key_snapshot(session, serial);
|
||||
self.store.store(
|
||||
key,
|
||||
snapshot,
|
||||
Info::now("server", "notification")
|
||||
).map_err(Error::Keystore)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Publishing
|
||||
///
|
||||
impl RrdpServer {
|
||||
|
||||
fn verified_rel(
|
||||
uri: &uri::Rsync,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<String, Error> {
|
||||
match uri.relative_to(base_uri) {
|
||||
Some(rel) => unsafe { // uri ensures characters are safe
|
||||
Ok(std::str::from_utf8_unchecked(rel).to_string())
|
||||
},
|
||||
None => Err(Error::OutsideBaseUri)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Process an update PublishQuery and produce a new delta, snapshot
|
||||
/// and notification file. Assumes that this is called *after* the
|
||||
/// ['FileStore'] has published, so files should already be saved to
|
||||
/// disk and the snapshots can be derived from this.
|
||||
pub fn publish(
|
||||
&mut self,
|
||||
delta: &publication::PublishDelta,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let (session, serial, deltas) = match self.get_notification()? {
|
||||
Some(notification) => {
|
||||
(
|
||||
notification.session_id().clone(),
|
||||
notification.serial(),
|
||||
notification.deltas().clone()
|
||||
)
|
||||
},
|
||||
None => {
|
||||
(
|
||||
{
|
||||
use rand::{thread_rng, Rng};
|
||||
let mut rng = thread_rng();
|
||||
let rnd: u32 = rng.gen();
|
||||
format!("{}", rnd)
|
||||
},
|
||||
0,
|
||||
Vec::new()
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let mut all_objects = match self.get_snapshot(&session, serial)? {
|
||||
Some(snapshot) => snapshot.objects().clone(),
|
||||
None => HashMap::new()
|
||||
};
|
||||
|
||||
let mut objects = match all_objects.get(&base_uri.to_string()) {
|
||||
Some(objects) => objects.clone(),
|
||||
None => Vec::new()
|
||||
};
|
||||
|
||||
for p in delta.publishes() {
|
||||
let _rel = Self::verified_rel(p.uri(), base_uri)?;
|
||||
let object = PublishedObject::new(p.uri().clone(), p.content().clone());
|
||||
if objects.contains(&object) {
|
||||
return Err(Error::ObjectAlreadyPresent(p.uri().clone()))
|
||||
}
|
||||
objects.push(object);
|
||||
}
|
||||
|
||||
for u in delta.updates() {
|
||||
let _rel = Self::verified_rel(u.uri(), base_uri)?;
|
||||
|
||||
match objects.iter().position(|cur| {cur.uri() == u.uri()}) {
|
||||
None => return Err(Error::NoObjectPresent(u.uri().clone())),
|
||||
Some(pos) => {
|
||||
if objects[pos].hash() != u.hash() {
|
||||
return Err(Error::NoObjectMatchingHash)
|
||||
} else {
|
||||
objects.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let object = PublishedObject::new(u.uri().clone(), u.content().clone());
|
||||
objects.push(object);
|
||||
}
|
||||
|
||||
for w in delta.withdraws() {
|
||||
let _rel = Self::verified_rel(w.uri(), base_uri)?;
|
||||
match objects.iter().position(|cur| {cur.uri() == w.uri()}) {
|
||||
None => return Err(Error::NoObjectPresent(w.uri().clone())),
|
||||
Some(pos) => {
|
||||
if objects[pos].hash() != w.hash() {
|
||||
return Err(Error::NoObjectMatchingHash)
|
||||
} else {
|
||||
objects.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
all_objects.insert(base_uri.to_string(), objects);
|
||||
|
||||
let new_serial = serial + 1;
|
||||
|
||||
// Create new snapshot
|
||||
let snapshot = Snapshot::new(
|
||||
session.clone(),
|
||||
new_serial,
|
||||
all_objects
|
||||
);
|
||||
|
||||
// Save the xml to disk
|
||||
let snapshot_ref = {
|
||||
let xml = snapshot.to_xml();
|
||||
let path = self.snapshot_path(&session, new_serial);
|
||||
file::save(&Bytes::from(xml), &path)?;
|
||||
SnapshotRef::new(
|
||||
FileInfo::for_path_and_uri(
|
||||
&path, self.snapshot_uri(&session, new_serial)
|
||||
)?
|
||||
)
|
||||
};
|
||||
|
||||
// save to store
|
||||
self.save_snapshot(&session, new_serial, snapshot)?;
|
||||
|
||||
// Create new delta
|
||||
let delta_ref = self.save_delta(&session, new_serial, delta)?;
|
||||
|
||||
// Create new notification file
|
||||
let new_notification = {
|
||||
let mut builder = NotificationBuilder::new();
|
||||
builder.with_session_id(session);
|
||||
builder.with_serial(new_serial);
|
||||
builder.with_deltas(deltas);
|
||||
builder.add_delta_to_start(delta_ref);
|
||||
builder.with_snapshot(snapshot_ref);
|
||||
builder.build()
|
||||
};
|
||||
|
||||
|
||||
let path = self.notification_path();
|
||||
new_notification.save(&path)?;
|
||||
|
||||
self.save_notification(new_notification)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Saves the RFC8181 PublishQuery as an RFC8182 delta file.
|
||||
fn save_delta(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
serial: usize,
|
||||
delta: &publication::PublishDelta
|
||||
) -> Result<DeltaRef, Error>
|
||||
{
|
||||
let path = self.delta_path(session_id, serial);
|
||||
debug!("Writing delta: {}", path.to_string_lossy());
|
||||
let mut file = file::create_file_with_path(&path)?;
|
||||
|
||||
XmlWriter::encode_to_file(& mut file, |w| {
|
||||
|
||||
let a = [
|
||||
("xmlns", NS),
|
||||
("version", VERSION),
|
||||
("session_id", session_id),
|
||||
("serial", &format!("{}", serial)),
|
||||
];
|
||||
|
||||
w.put_element(
|
||||
"delta",
|
||||
Some(&a),
|
||||
|w| {
|
||||
for publish in delta.publishes() {
|
||||
let uri = publish.uri().to_string();
|
||||
let a = [
|
||||
("uri", uri.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"publish",
|
||||
Some(&a),
|
||||
|w| {
|
||||
w.put_blob(publish.content())
|
||||
}
|
||||
)?
|
||||
}
|
||||
|
||||
for update in delta.updates() {
|
||||
let uri = update.uri().to_string();
|
||||
let hash = hex::encode(update.hash());
|
||||
let a = [
|
||||
("uri", uri.as_ref()),
|
||||
("hash", hash.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"publish",
|
||||
Some(&a),
|
||||
|w| {
|
||||
w.put_blob(update.content())
|
||||
}
|
||||
)?
|
||||
}
|
||||
|
||||
for withdraw in delta.withdraws() {
|
||||
let uri = withdraw.uri().to_string();
|
||||
let hash = hex::encode(withdraw.hash());
|
||||
let a = [
|
||||
("uri", uri.as_ref()),
|
||||
("hash", hash.as_ref())
|
||||
];
|
||||
w.put_element(
|
||||
"withdraw",
|
||||
Some(&a),
|
||||
|w| {
|
||||
w.empty()
|
||||
}
|
||||
)?
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
|
||||
let file_info = FileInfo::for_path(
|
||||
&path,
|
||||
&self.base_uri,
|
||||
&self.rrdp_base
|
||||
)?;
|
||||
Ok(DeltaRef::new(serial, file_info))
|
||||
}
|
||||
|
||||
pub fn notification_uri(&self) -> uri::Http {
|
||||
uri::Http::from_string(
|
||||
format!("{}notification.xml", self.base_uri.to_string())
|
||||
).unwrap() // Cannot fail. Config checked at startup.
|
||||
}
|
||||
|
||||
pub fn snapshot_uri(&self, session: &str, serial: usize) -> uri::Http {
|
||||
uri::Http::from_string(
|
||||
format!("{}{}/{}/snapshot.xml",
|
||||
self.base_uri.to_string(),
|
||||
session,
|
||||
serial
|
||||
)
|
||||
).unwrap() // Cannot fail. Config checked at startup.
|
||||
}
|
||||
|
||||
pub fn delta_uri(&self, session: &str, serial: usize) -> uri::Http {
|
||||
uri::Http::from_string(
|
||||
format!("{}{}/{}/snapshot.xml",
|
||||
self.base_uri.to_string(),
|
||||
session,
|
||||
serial
|
||||
)
|
||||
).unwrap() // Cannot fail. Config checked at startup.
|
||||
}
|
||||
|
||||
pub fn notification_path(&self) -> PathBuf {
|
||||
let mut path = self.rrdp_base.clone();
|
||||
path.push("notification.xml");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn delta_path(&self, session: &str, serial: usize) -> PathBuf {
|
||||
let mut path = self.serial_path(session, serial);
|
||||
path.push("delta.xml");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn snapshot_path(&self, session: &str, serial: usize) -> PathBuf {
|
||||
let mut path = self.serial_path(session, serial);
|
||||
path.push("snapshot.xml");
|
||||
path
|
||||
}
|
||||
|
||||
fn serial_path(&self, session: &str, serial: usize) -> PathBuf {
|
||||
let mut path = self.rrdp_base.clone();
|
||||
path.push(session);
|
||||
path.push(format!("{}", serial));
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum Error {
|
||||
#[display(fmt="{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
Keystore(keystore::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
RecursorError(RecursorError),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
UriError(uri::Error),
|
||||
|
||||
#[display(fmt="File already exists for uri (use update!): {}", _0)]
|
||||
ObjectAlreadyPresent(uri::Rsync),
|
||||
|
||||
#[display(fmt="Np file present for uri: {}", _0)]
|
||||
NoObjectPresent(uri::Rsync),
|
||||
|
||||
#[display(fmt="File does not match hash")]
|
||||
NoObjectMatchingHash,
|
||||
|
||||
#[display(fmt="Publishing outside of base URI is not allowed.")]
|
||||
OutsideBaseUri,
|
||||
|
||||
#[display(fmt="Issue deriving RRDP URI, check config. Base URI must end with a '/'!")]
|
||||
UriConfigError,
|
||||
|
||||
#[display(fmt="Error deserializing existing notification.xml")]
|
||||
NotificationFileError,
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
ParseIntError(ParseIntError),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
RrdpData(rrdp_data::Error)
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self { Error::IoError(e) }
|
||||
}
|
||||
|
||||
impl From<keystore::Error> for Error {
|
||||
fn from(e: keystore::Error) -> Self { Error::Keystore(e) }
|
||||
}
|
||||
|
||||
impl From<uri::Error> for Error {
|
||||
fn from(e: uri::Error) -> Self { Error::UriError(e) }
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for Error {
|
||||
fn from(e: ParseIntError) -> Self { Error::ParseIntError(e) }
|
||||
}
|
||||
|
||||
impl From<rrdp_data::Error> for Error {
|
||||
fn from(e: rrdp_data::Error) -> Self { Error::RrdpData(e) }
|
||||
}
|
||||
|
||||
impl From<RecursorError> for Error {
|
||||
fn from(e: RecursorError) -> Self { Error::RecursorError(e) }
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
use std::{io, fs};
|
||||
use std::path::PathBuf;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::krilld::pubd::RSYNC_FOLDER;
|
||||
use crate::util::file::{self, CurrentFile, RecursorError};
|
||||
|
||||
|
||||
//------------ FileStore -----------------------------------------------------
|
||||
|
||||
/// This type is responsible for publishing files on disk in a structure so
|
||||
/// that an rscynd can be set up to serve this (RPKI) data. Note that the
|
||||
/// rsync host name and module are part of the path, so make sure that the
|
||||
/// rsyncd modules and paths are setup properly for each supported rsync
|
||||
/// base uri used.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileStore {
|
||||
base_dir: PathBuf
|
||||
}
|
||||
|
||||
/// # Construct
|
||||
///
|
||||
impl FileStore {
|
||||
pub fn build(work_dir: &PathBuf) -> Result<Self, Error> {
|
||||
let mut rsync_dir = PathBuf::from(work_dir);
|
||||
rsync_dir.push(RSYNC_FOLDER);
|
||||
if ! rsync_dir.is_dir() {
|
||||
fs::create_dir_all(&rsync_dir)?;
|
||||
}
|
||||
Ok ( FileStore { base_dir: rsync_dir } )
|
||||
}
|
||||
}
|
||||
|
||||
/// # Publishing
|
||||
///
|
||||
impl FileStore {
|
||||
/// Process a PublishQuery update
|
||||
pub fn publish(
|
||||
&mut self,
|
||||
delta: &publication::PublishDelta,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<(), Error> {
|
||||
self.verify_query(delta, base_uri)?;
|
||||
self.update_files(delta)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list(
|
||||
&self,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<Vec<CurrentFile>, Error> {
|
||||
let path = self.path_for_publisher(base_uri);
|
||||
|
||||
if !path.exists() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
file::crawl_incl_rsync_base(&path, base_uri)
|
||||
.map_err(Error::RecursorError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that all updates are confined to the given base_uri; i.e. do
|
||||
/// not allow publishers to update things outside of their own jail.
|
||||
///
|
||||
/// Note this is done as a separate check, because there is a requirement
|
||||
/// that in case of any errors in an update, nothing is published. So,
|
||||
/// checking this first is a good enough form of a poor-man's transaction.
|
||||
fn verify_query(
|
||||
&self,
|
||||
delta: &publication::PublishDelta,
|
||||
base_uri: &uri::Rsync
|
||||
) -> Result<(), Error> {
|
||||
|
||||
for p in delta.publishes() {
|
||||
Self::assert_uri(base_uri, p.uri())?;
|
||||
if self.get_current_file_opt(p.uri()).is_some() {
|
||||
return Err(Error::ObjectAlreadyPresent(p.uri().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
for u in delta.updates() {
|
||||
Self::assert_uri(base_uri, u.uri())?;
|
||||
if let Some(cur) = self.get_current_file_opt(u.uri()) {
|
||||
if cur.hash() != u.hash() {
|
||||
return Err(Error::NoObjectMatchingHash)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoObjectPresent(u.uri().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
for w in delta.withdraws() {
|
||||
Self::assert_uri(base_uri, w.uri())?;
|
||||
if let Some(cur) = self.get_current_file_opt(w.uri()) {
|
||||
if cur.hash() != w.hash() {
|
||||
return Err(Error::NoObjectMatchingHash)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoObjectPresent(w.uri().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Update is consistent with current state");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform the actual updates on disk. This assumes that the updates
|
||||
/// have been verified. This can still blow up if there is an I/O issue
|
||||
/// writing to disk.
|
||||
fn update_files(
|
||||
&self,
|
||||
delta: &publication::PublishDelta
|
||||
) -> Result<(), Error> {
|
||||
for p in delta.publishes() {
|
||||
debug!("Saving file for uri: {}", p.uri().to_string());
|
||||
file::save_with_rsync_uri(
|
||||
p.content(),
|
||||
&self.base_dir,
|
||||
p.uri()
|
||||
)?;
|
||||
}
|
||||
|
||||
for u in delta.updates() {
|
||||
debug!("Updating file for uri: {}", u.uri().to_string());
|
||||
file::save_with_rsync_uri(
|
||||
u.content(),
|
||||
&self.base_dir,
|
||||
u.uri()
|
||||
)?;
|
||||
}
|
||||
|
||||
for w in delta.withdraws() {
|
||||
debug!("Withdrawing file for uri: {}", w.uri().to_string());
|
||||
file::delete_with_rsync_uri(
|
||||
&self.base_dir,
|
||||
w.uri()
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_uri(base: &uri::Rsync, file: &uri::Rsync) -> Result<(), Error> {
|
||||
if base.module() == file.module() &&
|
||||
file.path().starts_with(base.path()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::OutsideBaseUri)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_file_opt(
|
||||
&self,
|
||||
file_uri: &uri::Rsync
|
||||
) -> Option<CurrentFile> {
|
||||
match file::read_with_rsync_uri(&self.base_dir, file_uri) {
|
||||
Ok(bytes) => Some(CurrentFile::new(file_uri.clone(), bytes)),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the relative sub-dir that we should scan for this particular
|
||||
// publisher.
|
||||
fn path_for_publisher(&self, file_uri: &uri::Rsync) -> PathBuf {
|
||||
let mut path = self.base_dir.clone();
|
||||
let module = file_uri.module();
|
||||
path.push(PathBuf::from(module.authority()));
|
||||
path.push(PathBuf::from(module.module()));
|
||||
path.push(PathBuf::from(file_uri.path()));
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum Error {
|
||||
#[display(fmt = "{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt="{}", _0)]
|
||||
RecursorError(RecursorError),
|
||||
|
||||
#[display(fmt="File already exists for uri (use update!): {}", _0)]
|
||||
ObjectAlreadyPresent(uri::Rsync),
|
||||
|
||||
#[display(fmt="Np file present for uri: {}", _0)]
|
||||
NoObjectPresent(uri::Rsync),
|
||||
|
||||
#[display(fmt="File does not match hash")]
|
||||
NoObjectMatchingHash,
|
||||
|
||||
#[display(fmt="Publishing outside of base URI is not allowed.")]
|
||||
OutsideBaseUri,
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Error::IoError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecursorError> for Error {
|
||||
fn from(e: RecursorError) -> Self { Error::RecursorError(e) }
|
||||
}
|
||||
@@ -28,6 +28,7 @@ extern crate ring;
|
||||
extern crate untrusted;
|
||||
|
||||
pub mod api;
|
||||
pub mod eventsourcing;
|
||||
pub mod krillc;
|
||||
pub mod krilld;
|
||||
pub mod pubc;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use std::path::PathBuf;
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::api::publication_data;
|
||||
use crate::pubc;
|
||||
use crate::util::httpclient;
|
||||
|
||||
@@ -190,7 +190,7 @@ impl Format {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ApiResponse {
|
||||
Success,
|
||||
List(publication::ListReply),
|
||||
List(publication_data::ListReply),
|
||||
}
|
||||
|
||||
impl ApiResponse {
|
||||
@@ -211,7 +211,7 @@ impl ApiResponse {
|
||||
ApiResponse::List(list) => {
|
||||
for el in list.elements() {
|
||||
println!("{} {}",
|
||||
hex::encode(el.hash()),
|
||||
el.hash().to_string(),
|
||||
el.uri().to_string()
|
||||
);
|
||||
}
|
||||
@@ -238,14 +238,14 @@ pub fn execute(options: Options) -> Result<ApiResponse, Error> {
|
||||
}
|
||||
|
||||
|
||||
fn list_query(connection: &Connection) -> Result<publication::ListReply, Error> {
|
||||
fn list_query(connection: &Connection) -> Result<publication_data::ListReply, Error> {
|
||||
let uri = format!(
|
||||
"{}publication/{}",
|
||||
&connection.server_uri.to_string(),
|
||||
&connection.handle
|
||||
);
|
||||
|
||||
match httpclient::get_json::<publication::ListReply>(
|
||||
match httpclient::get_json::<publication_data::ListReply>(
|
||||
&uri,
|
||||
Some(&connection.token)
|
||||
) {
|
||||
|
||||
@@ -8,7 +8,8 @@ use clap::{App, Arg, SubCommand};
|
||||
use rpki::x509::ValidationError;
|
||||
use rpki::crypto::{PublicKeyFormat, Signer};
|
||||
use toml;
|
||||
use crate::api::publication;
|
||||
use crate::api::publication_data;
|
||||
use crate::pubc;
|
||||
use crate::remote::builder;
|
||||
use crate::remote::builder::{IdCertBuilder, SignedMessageBuilder};
|
||||
use crate::remote::id::{MyIdentity, MyRepoInfo, ParentInfo};
|
||||
@@ -19,7 +20,6 @@ use crate::storage::caching_ks::CachingDiskKeyStore;
|
||||
use crate::storage::keystore::{self, Info, Key, KeyStore};
|
||||
use crate::util::httpclient;
|
||||
use crate::util::softsigner::{self, OpenSslSigner};
|
||||
use pubc;
|
||||
|
||||
|
||||
/// # Some constants for naming resources in the keystore for clients.
|
||||
@@ -178,7 +178,7 @@ impl PubClient {
|
||||
/// validly signed and all.
|
||||
pub fn get_server_list(
|
||||
&mut self
|
||||
) -> Result<publication::ListReply, Error> {
|
||||
) -> Result<publication_data::ListReply, Error> {
|
||||
let query = rfc8181::Message::list_query();
|
||||
let signed_request = self.sign_request(query)?;
|
||||
|
||||
|
||||
+5
-5
@@ -3,15 +3,15 @@ pub mod cmsclient;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::api::publication_data;
|
||||
use crate::util::file;
|
||||
|
||||
pub fn create_delta(
|
||||
list_reply: &publication::ListReply,
|
||||
list_reply: &publication_data::ListReply,
|
||||
dir: &PathBuf,
|
||||
base_rsync: &uri::Rsync
|
||||
) -> Result<publication::PublishDelta, Error> {
|
||||
let mut delta_builder = publication::PublishDeltaBuilder::new();
|
||||
) -> Result<publication_data::PublishDelta, Error> {
|
||||
let mut delta_builder = publication_data::PublishDeltaBuilder::new();
|
||||
|
||||
let current = file::crawl_incl_rsync_base(dir, base_rsync)?;
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn create_delta(
|
||||
for p in list_reply.elements() {
|
||||
if current.iter().find(|c| c.uri() == p.uri()).is_none() {
|
||||
delta_builder.add_withdraw(
|
||||
publication::Withdraw::from_list_element(p)
|
||||
publication_data::Withdraw::from_list_element(p)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-31
@@ -1,12 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
use std::path::PathBuf;
|
||||
use bcder::Captured;
|
||||
use rpki::uri;
|
||||
use rpki::x509::ValidationError;
|
||||
use crate::api::publishers;
|
||||
use crate::api::publication;
|
||||
use crate::krilld::pubd::repo;
|
||||
use crate::krilld::pubd::rsyncd;
|
||||
use crate::api::publication_data;
|
||||
use crate::krilld::pubd;
|
||||
use crate::krilld::pubd::publishers::Publisher;
|
||||
use crate::remote::id::IdCert;
|
||||
use crate::remote::responder;
|
||||
use crate::remote::responder::Responder;
|
||||
@@ -52,7 +50,7 @@ impl CmsProxy {
|
||||
&mut self,
|
||||
msg: &SignedMessage,
|
||||
id_cert: &IdCert,
|
||||
) -> Result<publication::PublishRequest, Error> {
|
||||
) -> Result<publication_data::PublishRequest, Error> {
|
||||
debug!("Validating Signed Message");
|
||||
msg.validate(id_cert)?;
|
||||
let msg = rfc8181::Message::from_signed_message(&msg)?;
|
||||
@@ -64,13 +62,13 @@ impl CmsProxy {
|
||||
/// in signed CMS
|
||||
pub fn wrap_publish_reply(
|
||||
&mut self,
|
||||
reply: publication::PublishReply
|
||||
reply: publication_data::PublishReply
|
||||
) -> Result<Captured, Error> {
|
||||
let msg = match reply {
|
||||
publication::PublishReply::Success => {
|
||||
publication_data::PublishReply::Success => {
|
||||
rfc8181::Message::success_reply()
|
||||
},
|
||||
publication::PublishReply::List(list) => {
|
||||
publication_data::PublishReply::List(list) => {
|
||||
rfc8181::Message::list_reply(list)
|
||||
}
|
||||
};
|
||||
@@ -98,7 +96,7 @@ impl CmsProxy {
|
||||
/// Returns an RFC8183 Repository Response
|
||||
pub fn repository_response(
|
||||
&self,
|
||||
publisher: &Arc<publishers::Publisher>,
|
||||
publisher: &Publisher,
|
||||
base_service_uri: &uri::Http,
|
||||
rrdp_notification_uri: uri::Http
|
||||
) -> Result<rfc8183::RepositoryResponse, Error> {
|
||||
@@ -106,22 +104,20 @@ impl CmsProxy {
|
||||
let service_uri = format!(
|
||||
"{}rfc8181/{}",
|
||||
base_service_uri.to_string(),
|
||||
publisher.handle()
|
||||
publisher.id()
|
||||
);
|
||||
let service_uri = uri::Http::from_string(service_uri).unwrap();
|
||||
|
||||
self.responder
|
||||
.repository_response(
|
||||
&publisher,
|
||||
publisher,
|
||||
service_uri,
|
||||
rrdp_notification_uri)
|
||||
.map_err(Error::ResponderError)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//------------ Error ---------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
@@ -176,22 +172,8 @@ impl ToReportErrorCode for rfc8181::MessageError {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToReportErrorCode for repo::Error {
|
||||
impl ToReportErrorCode for pubd::Error {
|
||||
fn to_report_error_code(&self) -> rfc8181::ReportErrorCode {
|
||||
match self {
|
||||
repo::Error::Rsyncd(
|
||||
rsyncd::Error::ObjectAlreadyPresent(_)) =>
|
||||
rfc8181::ReportErrorCode::ObjectAlreadyPresent,
|
||||
repo::Error::Rsyncd(
|
||||
rsyncd::Error::NoObjectPresent(_)) =>
|
||||
rfc8181::ReportErrorCode::NoObjectPresent,
|
||||
repo::Error::Rsyncd(
|
||||
rsyncd::Error::NoObjectMatchingHash) =>
|
||||
rfc8181::ReportErrorCode::NoObjectMatchingHash,
|
||||
repo::Error::Rsyncd(
|
||||
rsyncd::Error::OutsideBaseUri) =>
|
||||
rfc8181::ReportErrorCode::PermissionFailure,
|
||||
_ => rfc8181::ReportErrorCode::OtherError
|
||||
}
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
+15
-7
@@ -6,7 +6,7 @@ use bcder::{Captured, Mode};
|
||||
use bcder::encode::Values;
|
||||
use rpki::crypto::{PublicKeyFormat, Signer};
|
||||
use rpki::uri;
|
||||
use crate::api::publishers::Publisher;
|
||||
use crate::krilld::pubd::publishers::Publisher;
|
||||
use crate::remote::builder;
|
||||
use crate::remote::builder::{IdCertBuilder, SignedMessageBuilder};
|
||||
use crate::remote::id::MyIdentity;
|
||||
@@ -95,7 +95,7 @@ impl Responder {
|
||||
impl Responder {
|
||||
pub fn repository_response(
|
||||
&self,
|
||||
publisher: &Arc<Publisher>,
|
||||
publisher: &Publisher,
|
||||
service_uri: uri::Http,
|
||||
rrdp_notification_uri: uri::Http
|
||||
) -> Result<RepositoryResponse, Error> {
|
||||
@@ -107,7 +107,7 @@ impl Responder {
|
||||
};
|
||||
|
||||
|
||||
let handle = publisher.handle();
|
||||
let handle = publisher.id();
|
||||
let id_cert = my_id.id_cert().clone();
|
||||
|
||||
let sia_base = publisher.base_uri().clone();
|
||||
@@ -115,7 +115,7 @@ impl Responder {
|
||||
Ok(
|
||||
RepositoryResponse::new(
|
||||
tag,
|
||||
handle.clone(),
|
||||
handle.to_string(),
|
||||
id_cert,
|
||||
service_uri,
|
||||
sia_base,
|
||||
@@ -196,8 +196,11 @@ impl From<builder::Error<softsigner::SignerError>> for Error {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::publisher_data::CmsAuthData;
|
||||
use crate::api::publisher_data::PublisherRequest;
|
||||
use crate::eventsourcing::Aggregate;
|
||||
use crate::krilld::pubd::publishers::PublisherInit;
|
||||
use crate::util::test;
|
||||
use crate::api::publishers::CmsAuthData;
|
||||
|
||||
#[test]
|
||||
fn should_have_response_for_publisher() {
|
||||
@@ -214,12 +217,17 @@ mod tests {
|
||||
|
||||
let cms_auth = CmsAuthData::new(tag, id_cert);
|
||||
|
||||
let publisher = Arc::new(Publisher::new(
|
||||
let publisher_request = PublisherRequest::new(
|
||||
name,
|
||||
"token".to_string(),
|
||||
base_uri,
|
||||
Some(cms_auth)
|
||||
));
|
||||
);
|
||||
|
||||
let init = PublisherInit::from(publisher_request);
|
||||
|
||||
let publisher = Publisher::init(init).unwrap();
|
||||
|
||||
|
||||
let rrdp_uri = test::http_uri("http://host/rrdp/");
|
||||
|
||||
|
||||
+54
-49
@@ -3,7 +3,8 @@
|
||||
use std::io;
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::api::publication_data;
|
||||
use crate::api::{Base64, EncodedHash};
|
||||
use crate::remote::sigmsg::SignedMessage;
|
||||
use crate::util::xml::{
|
||||
Attributes,
|
||||
@@ -125,7 +126,7 @@ impl Message {
|
||||
///
|
||||
impl Message {
|
||||
|
||||
pub fn list_reply(reply: publication::ListReply) -> Self {
|
||||
pub fn list_reply(reply: publication_data::ListReply) -> Self {
|
||||
Message::ReplyMessage(ReplyMessage::ListReply(reply))
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ impl Message {
|
||||
Message::ReplyMessage(ReplyMessage::SuccessReply)
|
||||
}
|
||||
|
||||
pub fn publish_delta_query(delta: publication::PublishDelta) -> Self {
|
||||
pub fn publish_delta_query(delta: publication_data::PublishDelta) -> Self {
|
||||
Message::QueryMessage(QueryMessage::PublishDelta(delta))
|
||||
}
|
||||
|
||||
@@ -148,7 +149,7 @@ impl Message {
|
||||
/// This type represents query type Publication Messages defined in RFC8181
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum QueryMessage {
|
||||
PublishDelta(publication::PublishDelta),
|
||||
PublishDelta(publication_data::PublishDelta),
|
||||
ListQuery
|
||||
}
|
||||
|
||||
@@ -207,10 +208,10 @@ impl QueryMessage {
|
||||
}
|
||||
|
||||
/// Consumes this and returns this a PublishRequest for our (json) API
|
||||
pub fn into_publish_request(self) -> publication::PublishRequest {
|
||||
pub fn into_publish_request(self) -> publication_data::PublishRequest {
|
||||
match self {
|
||||
QueryMessage::ListQuery => publication::PublishRequest::List,
|
||||
QueryMessage::PublishDelta(d) => publication::PublishRequest::Delta(d)
|
||||
QueryMessage::ListQuery => publication_data::PublishRequest::List,
|
||||
QueryMessage::PublishDelta(d) => publication_data::PublishRequest::Delta(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,9 +225,9 @@ pub struct PublishDeltaXml;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PublishDeltaElement {
|
||||
Publish(publication::Publish),
|
||||
Update(publication::Update),
|
||||
Withdraw(publication::Withdraw)
|
||||
Publish(publication_data::Publish),
|
||||
Update(publication_data::Update),
|
||||
Withdraw(publication_data::Withdraw)
|
||||
}
|
||||
|
||||
impl PublishDeltaElement {
|
||||
@@ -257,17 +258,19 @@ impl PublishDeltaXml {
|
||||
|
||||
let uri = uri::Rsync::from_string(a.take_req("uri")?)?;
|
||||
let tag = a.take_req("tag")?;
|
||||
let object = r.take_bytes_characters()?;
|
||||
let base64_string = r.take_chars()?;
|
||||
let base64 = Base64::from(base64_string);
|
||||
|
||||
let res = match a.take_opt_hex("hash") {
|
||||
Some(hash) => {
|
||||
let update = publication::Update::new(
|
||||
Some(tag), uri, object, hash
|
||||
let res = match a.take_opt("hash") {
|
||||
Some(hash_str) => {
|
||||
let hash = EncodedHash::from(hash_str);
|
||||
let update = publication_data::Update::new(
|
||||
Some(tag), uri, base64, hash
|
||||
);
|
||||
Ok(PublishDeltaElement::Update(update))
|
||||
},
|
||||
None => {
|
||||
let publish = publication::Publish::new(Some(tag), uri, object);
|
||||
let publish = publication_data::Publish::new(Some(tag), uri, base64);
|
||||
Ok(PublishDeltaElement::Publish(publish))
|
||||
}
|
||||
};
|
||||
@@ -281,13 +284,14 @@ impl PublishDeltaXml {
|
||||
a: &mut Attributes
|
||||
) -> Result<PublishDeltaElement, MessageError> {
|
||||
|
||||
let hash = a.take_req_hex("hash")?;
|
||||
let hash_str = a.take_req("hash")?;
|
||||
let hash = EncodedHash::from(hash_str);
|
||||
let uri = uri::Rsync::from_string(a.take_req("uri")?)?;
|
||||
let tag = a.take_req("tag")?;
|
||||
|
||||
a.exhausted()?;
|
||||
|
||||
let withdraw = publication::Withdraw::new(Some(tag), uri, hash);
|
||||
let withdraw = publication_data::Withdraw::new(Some(tag), uri, hash);
|
||||
Ok(PublishDeltaElement::Withdraw(withdraw))
|
||||
}
|
||||
|
||||
@@ -318,8 +322,8 @@ impl PublishDeltaXml {
|
||||
/// is processed by PublicationMessage::decode
|
||||
pub fn decode<R: io::Read>(
|
||||
r: &mut XmlReader<R>
|
||||
) -> Result<publication::PublishDelta, MessageError> {
|
||||
let mut bld = publication::PublishDeltaBuilder::new();
|
||||
) -> Result<publication_data::PublishDelta, MessageError> {
|
||||
let mut bld = publication_data::PublishDeltaBuilder::new();
|
||||
|
||||
while let Some(pde) = Self::decode_opt(r)? {
|
||||
match pde {
|
||||
@@ -339,7 +343,7 @@ impl PublishDeltaXml {
|
||||
/// Encodes a PublishDelta to XML in the given writer, for inclusion in an
|
||||
/// RFC8181 CMS.
|
||||
pub fn encode<W: io::Write>(
|
||||
delta: &publication::PublishDelta,
|
||||
delta: &publication_data::PublishDelta,
|
||||
w: &mut XmlWriter<W>
|
||||
) -> Result<(), io::Error> {
|
||||
for p in delta.publishes() { Self::encode_publish(p, w)?; }
|
||||
@@ -349,12 +353,13 @@ impl PublishDeltaXml {
|
||||
}
|
||||
|
||||
fn encode_publish<W: io::Write>(
|
||||
publish: &publication::Publish,
|
||||
publish: &publication_data::Publish,
|
||||
w: &mut XmlWriter<W>
|
||||
) -> Result<(), io::Error> {
|
||||
|
||||
let uri = publish.uri().to_string();
|
||||
let tag = publish.tag_for_xml();
|
||||
let content = publish.content().to_string();
|
||||
|
||||
let a = [
|
||||
("tag", tag.as_ref()),
|
||||
@@ -365,23 +370,22 @@ impl PublishDeltaXml {
|
||||
"publish",
|
||||
Some(&a),
|
||||
|w| {
|
||||
w.put_blob(publish.content())
|
||||
w.put_text(content.as_ref())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_update<W: io::Write>(
|
||||
update: &publication::Update,
|
||||
update: &publication_data::Update,
|
||||
w: &mut XmlWriter<W>
|
||||
) -> Result<(), io::Error> {
|
||||
|
||||
let uri = update.uri().to_string();
|
||||
let enc = hex::encode(update.hash());
|
||||
let tag = update.tag_for_xml();
|
||||
|
||||
let a = [
|
||||
("tag", tag.as_ref()),
|
||||
("hash", enc.as_ref()),
|
||||
("hash", update.hash().as_ref()),
|
||||
("uri", uri.as_ref())
|
||||
];
|
||||
|
||||
@@ -389,22 +393,21 @@ impl PublishDeltaXml {
|
||||
"publish",
|
||||
Some(&a),
|
||||
|w| {
|
||||
w.put_blob(update.content())
|
||||
w.put_text(update.content().as_ref())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_withdraw<W: io::Write>(
|
||||
withdraw: &publication::Withdraw,
|
||||
withdraw: &publication_data::Withdraw,
|
||||
w: &mut XmlWriter<W>
|
||||
) -> Result<(), io::Error> {
|
||||
|
||||
let uri = withdraw.uri().to_string();
|
||||
let hash = hex::encode(withdraw.hash());
|
||||
let tag = withdraw.tag_for_xml();
|
||||
|
||||
let a = [
|
||||
("hash", hash.as_ref()),
|
||||
("hash", withdraw.hash().as_ref()),
|
||||
("tag", tag.as_ref()),
|
||||
("uri", uri.as_ref())
|
||||
];
|
||||
@@ -426,7 +429,7 @@ impl PublishDeltaXml {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ReplyMessage {
|
||||
SuccessReply,
|
||||
ListReply(publication::ListReply),
|
||||
ListReply(publication_data::ListReply),
|
||||
ErrorReply(ErrorReply)
|
||||
}
|
||||
|
||||
@@ -468,7 +471,7 @@ impl ReplyMessage {
|
||||
/// Decodes XML to a ListReply.
|
||||
fn decode_list_reply<R: io::Read>(
|
||||
r: &mut XmlReader<R>
|
||||
) -> Result<publication::ListReply, MessageError> {
|
||||
) -> Result<publication_data::ListReply, MessageError> {
|
||||
|
||||
let mut elements = vec![];
|
||||
|
||||
@@ -476,11 +479,11 @@ impl ReplyMessage {
|
||||
let e = r.take_opt_element(|t, mut a, _r| {
|
||||
match t.name.as_ref() {
|
||||
"list" => {
|
||||
let hash = a.take_req_hex("hash")?;
|
||||
let hash = EncodedHash::from(a.take_req("hash")?);
|
||||
let uri = uri::Rsync::from_string(a.take_req("uri")?)?;
|
||||
a.exhausted()?;
|
||||
|
||||
Ok(Some(publication::ListElement::new(uri, hash)))
|
||||
Ok(Some(publication_data::ListElement::new(uri, hash)))
|
||||
},
|
||||
_ => {
|
||||
Err(MessageError::UnexpectedStart(t.name.clone()))
|
||||
@@ -493,7 +496,7 @@ impl ReplyMessage {
|
||||
None => break
|
||||
}
|
||||
}
|
||||
Ok(publication::ListReply::new(elements))
|
||||
Ok(publication_data::ListReply::new(elements))
|
||||
}
|
||||
|
||||
/// Encodes a ReplyMessage for inclusion in an RFC8181 Protocol CMS.
|
||||
@@ -512,17 +515,16 @@ impl ReplyMessage {
|
||||
|
||||
/// Encodes a ListReply to XML.
|
||||
fn encode_list_reply<W: io::Write>(
|
||||
reply: &publication::ListReply,
|
||||
reply: &publication_data::ListReply,
|
||||
w: &mut XmlWriter<W>
|
||||
) -> Result<(), io::Error> {
|
||||
|
||||
for el in reply.elements() {
|
||||
let hash = hex::encode(&el.hash());
|
||||
let uri = el.uri().to_string();
|
||||
|
||||
w.put_element(
|
||||
"list",
|
||||
Some(&[("hash", hash.as_ref()), ("uri", uri.as_ref())]),
|
||||
Some(&[("hash", el.hash().as_ref()), ("uri", uri.as_ref())]),
|
||||
|w| { w.empty() }
|
||||
)?;
|
||||
}
|
||||
@@ -892,13 +894,13 @@ mod tests {
|
||||
use super::*;
|
||||
use std::str;
|
||||
use rpki::uri::Rsync;
|
||||
use crate::util::sha256;
|
||||
use crate::api::EncodedHash;
|
||||
|
||||
//------------ ListReplyBuilder ----------------------------------------------
|
||||
|
||||
/// This type is useful for testing
|
||||
pub struct ListReplyBuilder {
|
||||
elements: Vec<publication::ListElement>
|
||||
elements: Vec<publication_data::ListElement>
|
||||
}
|
||||
|
||||
impl ListReplyBuilder {
|
||||
@@ -908,8 +910,8 @@ mod tests {
|
||||
}
|
||||
|
||||
pub fn add(&mut self, object: &Bytes, uri: uri::Rsync) {
|
||||
let hash = sha256(object);
|
||||
let el = publication::ListElement::new(uri, hash);
|
||||
let hash = EncodedHash::from_content(object);
|
||||
let el = publication_data::ListElement::new(uri, hash);
|
||||
self.elements.push(el);
|
||||
}
|
||||
|
||||
@@ -917,7 +919,7 @@ mod tests {
|
||||
/// protocol CMS message.
|
||||
pub fn build_message(self) -> Message {
|
||||
Message::list_reply(
|
||||
publication::ListReply::new(self.elements)
|
||||
publication_data::ListReply::new(self.elements)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1013,7 +1015,8 @@ mod tests {
|
||||
#[test]
|
||||
fn should_create_error_reply() {
|
||||
let object = Bytes::from_static(include_bytes!("../../test/remote/cms_ta.cer"));
|
||||
let publish = publication::Publish::with_hash_tag(
|
||||
let object = Base64::from_content(&object);
|
||||
let publish = publication_data::Publish::with_hash_tag(
|
||||
rsync_uri("rsync://host/path/cms-ta.cer"),
|
||||
object
|
||||
);
|
||||
@@ -1049,26 +1052,28 @@ mod tests {
|
||||
fn should_encode_publish_delta() {
|
||||
let object = Bytes::from_static(include_bytes!("../../test/remote/cms_ta.cer"));
|
||||
let object2 = Bytes::from_static(include_bytes!("../../test/remote/pdu_200.der"));
|
||||
let object_hash = sha256(&object);
|
||||
let object_hash = EncodedHash::from_content(&object);
|
||||
let object = Base64::from_content(&object);
|
||||
let object2 = Base64::from_content(&object2);
|
||||
|
||||
let mut builder = publication::PublishDeltaBuilder::new();
|
||||
let mut builder = publication_data::PublishDeltaBuilder::new();
|
||||
|
||||
builder.add_withdraw(
|
||||
publication::Withdraw::with_hash_tag(
|
||||
publication_data::Withdraw::with_hash_tag(
|
||||
rsync_uri("rsync://host/path/cms-ta.cer"),
|
||||
object_hash.clone()
|
||||
)
|
||||
);
|
||||
|
||||
builder.add_publish(
|
||||
publication::Publish::with_hash_tag(
|
||||
publication_data::Publish::with_hash_tag(
|
||||
rsync_uri("rsync://host/path/cms-ta.cer"),
|
||||
object
|
||||
)
|
||||
);
|
||||
|
||||
builder.add_update(
|
||||
publication::Update::with_hash_tag(
|
||||
publication_data::Update::with_hash_tag(
|
||||
rsync_uri("rsync://host/path/cms-ta.cer"),
|
||||
object2,
|
||||
object_hash
|
||||
|
||||
@@ -9,7 +9,7 @@ use bcder::decode;
|
||||
use rpki::uri;
|
||||
use rpki::x509;
|
||||
use rpki::x509::Time;
|
||||
use crate::api::publishers;
|
||||
use crate::api::publisher_data;
|
||||
use crate::remote::id::IdCert;
|
||||
use crate::util::xml::{AttributesError, XmlReader, XmlReaderErr, XmlWriter};
|
||||
|
||||
@@ -130,11 +130,11 @@ impl PublisherRequest {
|
||||
self,
|
||||
token: String,
|
||||
base_uri: uri::Rsync
|
||||
) -> publishers::Publisher {
|
||||
let cms_data = publishers::CmsAuthData::new(
|
||||
) -> publisher_data::PublisherRequest {
|
||||
let cms_data = publisher_data::CmsAuthData::new(
|
||||
self.tag, self.id_cert
|
||||
);
|
||||
publishers::Publisher::new(
|
||||
publisher_data::PublisherRequest::new(
|
||||
self.publisher_handle,
|
||||
token,
|
||||
base_uri,
|
||||
|
||||
@@ -1,676 +0,0 @@
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::RwLock;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json;
|
||||
use crate::util::file;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
//------------ Storable ------------------------------------------------------
|
||||
|
||||
pub trait Storable: Serialize + DeserializeOwned + Sized {}
|
||||
impl<T: Serialize + DeserializeOwned + Sized> Storable for T { }
|
||||
|
||||
|
||||
//------------ AggregateId ---------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct AggregateId(String);
|
||||
|
||||
impl AggregateId {
|
||||
pub fn new(s: &str) -> Self {
|
||||
AggregateId(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
//------------ Aggregate -----------------------------------------------------
|
||||
|
||||
pub trait Aggregate: Storable + Clone {
|
||||
|
||||
type Command: Command<Event = Self::Event>;
|
||||
type Event: Event;
|
||||
type InitEvent: Event;
|
||||
type Error: std::error::Error;
|
||||
|
||||
/// Creates a new instance. Expects an event with data needed to
|
||||
/// initialise the instance. Typically this means that a specific
|
||||
/// 'create' event is passed, with all the needed data, or just an empty
|
||||
/// marker if no data is needed. Implementations must return an error in
|
||||
/// case the instance cannot be created.
|
||||
fn init(event: Self::InitEvent) -> Result<Self, Self::Error>;
|
||||
|
||||
/// Returns the current version of the aggregate.
|
||||
fn version(&self) -> u64;
|
||||
|
||||
/// Moves this, and applies the event to this. This MUST not result in
|
||||
/// any errors. Applying the event just updates data and is side-effect
|
||||
/// free.
|
||||
///
|
||||
/// Note that both self and the event are moved. This is done because we
|
||||
/// want to enable moving data into the new aggregate without the need for
|
||||
/// additional allocations.
|
||||
fn apply(&mut self, event: Self::Event);
|
||||
|
||||
/// Processes a command. I.e. validate the command, and return a list of
|
||||
/// events that will result in the desired new state, but do not apply
|
||||
/// these event here. The command processing must be side-effect free.
|
||||
///
|
||||
/// The command is moved, because we want to enable moving its data
|
||||
/// without reallocating.
|
||||
fn process_command(&self, command: Self::Command) -> Result<Vec<Self::Event>, Self::Error>;
|
||||
}
|
||||
|
||||
|
||||
//------------ Event --------------------------------------------------------
|
||||
|
||||
pub trait Event: Storable {
|
||||
/// Identifies the aggregate, useful when storing and retrieving the event.
|
||||
fn id(&self) -> &AggregateId;
|
||||
|
||||
/// The version of the aggregate that this event updates.
|
||||
/// In other words, an aggregate that is currently at version x, will get
|
||||
/// version x + 1, when the event for version x is applied.
|
||||
fn version(&self) -> u64;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct StoredEvent<E: Storable> {
|
||||
id: AggregateId,
|
||||
version: u64,
|
||||
#[serde(deserialize_with = "E::deserialize")]
|
||||
details: E
|
||||
}
|
||||
|
||||
impl<E: Storable> StoredEvent<E> {
|
||||
pub fn new(id: &AggregateId, version: u64, event: E) -> Self {
|
||||
StoredEvent { id: id.clone(), version, details: event }
|
||||
}
|
||||
|
||||
pub fn event(&self) -> &E { & self.details }
|
||||
|
||||
pub fn into_details(self) -> E { self.details }
|
||||
|
||||
pub fn unwrap(self) -> (AggregateId, u64, E) {
|
||||
(self.id, self.version, self.details)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Storable> Event for StoredEvent<E> {
|
||||
fn id(&self) -> &AggregateId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
|
||||
//------------ Command -------------------------------------------------------
|
||||
|
||||
/// Commands are used to send an intent to change an aggregate.
|
||||
///
|
||||
/// Think of this as the data container for your update API, plus some
|
||||
/// meta-data to ensure that the command is sent to the right instance of an
|
||||
/// Aggregate, and that concurrency issues are handled.
|
||||
pub trait Command: Storable {
|
||||
/// Identify the type of event returned by the aggregate that uses this
|
||||
/// command. This is needed because we may need to check whether a
|
||||
/// command conflicts with recent events.
|
||||
type Event: Event;
|
||||
|
||||
/// Identifies the aggregate, useful when storing and retrieving the event.
|
||||
fn id(&self) -> &AggregateId;
|
||||
|
||||
/// The version of the aggregate that this command updates. If this
|
||||
/// command should update whatever the latest version happens to be, then
|
||||
/// use None here.
|
||||
fn version(&self) -> Option<u64>;
|
||||
|
||||
/// In case of concurrent processing of commands, the aggregate may be
|
||||
/// outdated when a command is applied. In such cases this method expects
|
||||
/// the list of events that happened since the ['affected_version'] and
|
||||
/// will return whether there is a conflict. If there is no conflict that
|
||||
/// the command may be applied again.
|
||||
///
|
||||
/// Note that this defaults to true, which is the safe choice when in
|
||||
/// doubt. If you choose to implement this, then you will also need to
|
||||
/// implement the ['set_affected_version'] function.
|
||||
fn conflicts(&self, _events: &[Self::Event]) -> bool { true }
|
||||
}
|
||||
|
||||
|
||||
//------------ SentCommand ---------------------------------------------------
|
||||
|
||||
/// Convenience wrapper so that implementations can just implement
|
||||
/// ['CommandDetails'] and leave the id and version boilerplate.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct SentCommand<C: CommandDetails> {
|
||||
id: AggregateId,
|
||||
version: Option<u64>,
|
||||
#[serde(deserialize_with = "C::deserialize")]
|
||||
details: C
|
||||
}
|
||||
|
||||
impl<C: CommandDetails> Command for SentCommand<C> {
|
||||
type Event = C::Event;
|
||||
|
||||
fn id(&self) -> &AggregateId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn version(&self) -> Option<u64> {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CommandDetails> SentCommand<C> {
|
||||
pub fn new(id: &AggregateId, version: Option<u64>, details: C) -> Self {
|
||||
SentCommand { id: id.clone(), version, details }
|
||||
}
|
||||
|
||||
pub fn into_details(self) -> C { self.details }
|
||||
}
|
||||
|
||||
|
||||
//------------ CommandDetails ------------------------------------------------
|
||||
|
||||
/// Implement this for an enum with CommandDetails, so you you can reuse the
|
||||
/// id and version boilerplate from ['SentCommand'].
|
||||
pub trait CommandDetails: Storable {
|
||||
type Event: Event;
|
||||
}
|
||||
|
||||
|
||||
//------------ AggregateRef --------------------------------------------------
|
||||
|
||||
/// This type wraps the Aggregate references returned by the AggregateManager,
|
||||
/// so that we can change the implementation details of the of the latter.
|
||||
/// This derefs to the Aggregate.
|
||||
pub struct AggregateRef<A: Aggregate> {
|
||||
agg: Arc<A>
|
||||
}
|
||||
|
||||
impl<A: Aggregate> Deref for AggregateRef<A> {
|
||||
type Target = A;
|
||||
|
||||
fn deref(&self) -> &'_ Self::Target {
|
||||
&self.agg
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Aggregate> AsRef<A> for AggregateRef<A> {
|
||||
fn as_ref(&self) -> &A {
|
||||
&self.agg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ AggregateManager ----------------------------------------------
|
||||
|
||||
/// This type is responsible for managing Aggregates. I.e. creating new
|
||||
/// Aggregate instances, returning a reference for reading, dispatching
|
||||
/// commands to an aggregate, and storing them.
|
||||
pub struct AggregateManager<A:Aggregate, S: KeyStore> {
|
||||
/// Stores a the most recent aggregate.
|
||||
///
|
||||
/// Note. We may wish to change this in future. E.g. just remember the
|
||||
/// version of the aggregate and get it from storage as needed. Or even
|
||||
/// make this a choice - keep things we use often in memory, but not
|
||||
/// other things.
|
||||
///
|
||||
/// We may then also wish to change the return types, and maybe we will
|
||||
/// have to push the write locking down to the keystore.. I.e. any new
|
||||
/// save already requires a new key - it is a write once store. So, if we
|
||||
/// get an error then that is an indication of a concurrency issue.
|
||||
cache: RwLock<HashMap<AggregateId, Arc<A>>>,
|
||||
store: S
|
||||
}
|
||||
|
||||
impl<A: 'static + Aggregate, S: 'static + KeyStore> AggregateManager<A, S> {
|
||||
|
||||
pub fn new(store: S) -> Self {
|
||||
let values = RwLock::new(HashMap::new());
|
||||
AggregateManager {
|
||||
cache: values, store
|
||||
}
|
||||
}
|
||||
|
||||
fn update_cache(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
mut force: bool
|
||||
) -> Result<(), AggMgrErr<A::Error, S::Error>> {
|
||||
|
||||
let mut cache = self.cache.write().unwrap();
|
||||
|
||||
let mut has_key = cache.contains_key(id);
|
||||
|
||||
if ! has_key {
|
||||
let init_key = S::key_for_event(id, 0);
|
||||
if let Some(init) = self.store.get::<A::InitEvent>(&init_key)
|
||||
.map_err(AggMgrErr::KeyStoreError)? {
|
||||
let mut agg = A::init(init).map_err(AggMgrErr::AggregateError)?;
|
||||
|
||||
cache.insert(id.clone(), Arc::new(agg));
|
||||
force = true;
|
||||
has_key = true;
|
||||
}
|
||||
}
|
||||
|
||||
if has_key && force {
|
||||
// We MUST have an entry now
|
||||
let arc = cache.get_mut(id).unwrap();
|
||||
let agg = Arc::make_mut(arc);
|
||||
|
||||
loop {
|
||||
let ver = agg.version();
|
||||
let key = S::key_for_event(id, ver);
|
||||
if let Some(event) = self.store.get::<A::Event>(&key)
|
||||
.map_err(AggMgrErr::KeyStoreError)? {
|
||||
agg.apply(event)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Get a reference to the latest version of the aggregate.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn get_latest(
|
||||
&self,
|
||||
id: &AggregateId
|
||||
) -> Result<Option<AggregateRef<A>>, AggMgrErr<A::Error, S::Error>> {
|
||||
self.update_cache(id, false)?;
|
||||
Ok(self.cache.read().unwrap().get(id)
|
||||
.map(|arc| AggregateRef { agg: arc.clone() } ))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn create(
|
||||
&self,
|
||||
id: &AggregateId,
|
||||
event: A::InitEvent
|
||||
) -> Result<(), AggMgrErr<A::Error, S::Error>> {
|
||||
self.update_cache(id, true)?;
|
||||
|
||||
let mut cache = self.cache.write().unwrap();
|
||||
if cache.contains_key(id) {
|
||||
Err(AggMgrErr::AggregateAlreadyExists)
|
||||
} else {
|
||||
let key = S::key_for_event(id, 0);
|
||||
self.store.store(&key, &event).map_err(AggMgrErr::KeyStoreError)?;
|
||||
|
||||
let agg = A::init(event).map_err(AggMgrErr::AggregateError)?;
|
||||
|
||||
cache.insert(id.clone(), Arc::new(agg));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a command to the latest aggregate, save the events and return
|
||||
/// the updated aggregate.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn apply(
|
||||
&self,
|
||||
command: A::Command
|
||||
) -> Result<(), AggMgrErr<A::Error, S::Error>> {
|
||||
let id = command.id().clone();
|
||||
self.update_cache(&id, true)?;
|
||||
|
||||
let mut cache = self.cache.write().unwrap();
|
||||
|
||||
match cache.get_mut(&id) {
|
||||
None => Err(AggMgrErr::AggregateDoesNotExist),
|
||||
Some(agg) => {
|
||||
|
||||
let agg = Arc::make_mut(agg);
|
||||
|
||||
if let Some(version) = command.version() {
|
||||
if version != agg.version() {
|
||||
// TODO check conflicts
|
||||
return Err(AggMgrErr::ConcurrentModification)
|
||||
}
|
||||
}
|
||||
|
||||
let events = agg.process_command(command)
|
||||
.map_err(AggMgrErr::AggregateError)?;
|
||||
|
||||
for e in events {
|
||||
let key = S::key_for_event(&id, e.version());
|
||||
self.store.store(&key, &e).map_err(AggMgrErr::KeyStoreError)?;
|
||||
agg.apply(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------ AggMgrErr -----------------------------------------------------
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum AggMgrErr<A: Display, K: Display> {
|
||||
#[display(fmt = "Aggregate does not exist")]
|
||||
AggregateDoesNotExist,
|
||||
|
||||
#[display(fmt = "Aggregate already exists")]
|
||||
AggregateAlreadyExists,
|
||||
|
||||
#[display(fmt = "Concurrent modification. Command rejected")]
|
||||
ConcurrentModification,
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
AggregateError(A),
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
KeyStoreError(K),
|
||||
}
|
||||
|
||||
|
||||
//------------ KeyStore ------------------------------------------------------
|
||||
|
||||
/// Generic KeyStore for AggregateManager
|
||||
pub trait KeyStore {
|
||||
type Key;
|
||||
type Error: std::error::Error;
|
||||
|
||||
fn key_for_snapshot(id: &AggregateId, version: u64) -> Self::Key;
|
||||
fn key_for_event(id: &AggregateId, version: u64) -> Self::Key;
|
||||
|
||||
/// Returns whether a key already exists.
|
||||
fn has_key(&self, key: &Self::Key) -> bool;
|
||||
|
||||
/// Throws an error if the key already exists.
|
||||
fn store<V: Any + Serialize>(&self, key: &Self::Key, value: &V) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the value for this key, if any exists.
|
||||
fn get<V: Any + Storable>(&self, key: &Self::Key) -> Result<Option<V>, Self::Error>;
|
||||
}
|
||||
|
||||
|
||||
//------------ DiskKeyStore --------------------------------------------------
|
||||
|
||||
/// This type can store and retrieve values to/from disk, using json
|
||||
/// serialization.
|
||||
pub struct DiskKeyStore {
|
||||
dir: PathBuf
|
||||
}
|
||||
|
||||
impl KeyStore for DiskKeyStore {
|
||||
type Key = PathBuf;
|
||||
type Error = DiskKeyStoreError;
|
||||
|
||||
fn key_for_snapshot(id: &AggregateId, version: u64) -> Self::Key {
|
||||
PathBuf::from(format!("snapshot-{}-{}", id.0, version))
|
||||
}
|
||||
|
||||
fn key_for_event(id: &AggregateId, version: u64) -> Self::Key {
|
||||
PathBuf::from(format!("delta-{}-{}", id.0, version))
|
||||
}
|
||||
|
||||
fn has_key(&self, key: &Self::Key) -> bool {
|
||||
self.file_path(key).exists()
|
||||
}
|
||||
|
||||
fn store<V: Any + Serialize>(&self, key: &Self::Key, value: &V) -> Result<(),
|
||||
Self::Error> {
|
||||
if self.has_key(key) {
|
||||
Err(DiskKeyStoreError::KeyExists(key.to_string_lossy().to_string()))
|
||||
} else {
|
||||
let mut f = file::create_file_with_path(&self.file_path(key))?;
|
||||
let json = serde_json::to_string(value)?;
|
||||
f.write_all(json.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get<V: Any + Storable>(&self, key: &Self::Key) -> Result<Option<V>, Self::Error> {
|
||||
if self.has_key(key) {
|
||||
let f = File::open(self.file_path(&key))?;
|
||||
let v: V = serde_json::from_reader(f)?;
|
||||
Ok(Some(v))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiskKeyStore {
|
||||
pub fn new(dir: PathBuf) -> Self {
|
||||
DiskKeyStore { dir }
|
||||
}
|
||||
|
||||
fn file_path(&self, key: &<Self as KeyStore>::Key) -> PathBuf {
|
||||
let mut file_path = self.dir.clone();
|
||||
file_path.push(key);
|
||||
file_path
|
||||
}
|
||||
}
|
||||
|
||||
//------------ DiskKeyStoreError ---------------------------------------------
|
||||
|
||||
/// This type defines possible Errors for KeyStore
|
||||
#[derive(Debug, Display)]
|
||||
pub enum DiskKeyStoreError {
|
||||
#[display(fmt = "{}", _0)]
|
||||
IoError(io::Error),
|
||||
|
||||
#[display(fmt = "{}", _0)]
|
||||
JsonError(serde_json::Error),
|
||||
|
||||
#[display(fmt = "Key already exists: {}", _0)]
|
||||
KeyExists(String)
|
||||
}
|
||||
|
||||
impl From<io::Error> for DiskKeyStoreError {
|
||||
fn from(e: io::Error) -> Self { DiskKeyStoreError::IoError(e) }
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for DiskKeyStoreError {
|
||||
fn from(e: serde_json::Error) -> Self { DiskKeyStoreError::JsonError(e) }
|
||||
}
|
||||
|
||||
impl std::error::Error for DiskKeyStoreError { }
|
||||
|
||||
|
||||
|
||||
//------------ Tests ---------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::util::test;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct Person {
|
||||
id: AggregateId,
|
||||
version: u64,
|
||||
name: String,
|
||||
age: u8
|
||||
}
|
||||
|
||||
impl Person {
|
||||
pub fn id(&self) -> &AggregateId { &self.id }
|
||||
pub fn version(&self) -> u64 { self.version }
|
||||
pub fn name(&self) -> &String { &self.name }
|
||||
pub fn age(&self) -> u8 { self.age }
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct InitPersonDetails {
|
||||
pub name: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
enum PersonEventDetails {
|
||||
NameChanged(String),
|
||||
HadBirthday
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
enum PersonCommandDetails {
|
||||
ChangeName(String),
|
||||
GoAroundTheSun
|
||||
}
|
||||
|
||||
impl CommandDetails for PersonCommandDetails {
|
||||
type Event = PersonEvent;
|
||||
}
|
||||
|
||||
type InitPersonEvent = StoredEvent<InitPersonDetails>;
|
||||
|
||||
impl InitPersonEvent {
|
||||
pub fn init(id: &AggregateId, name: &str) -> Self {
|
||||
StoredEvent::new(id, 0, InitPersonDetails { name: name.to_string()})
|
||||
}
|
||||
}
|
||||
|
||||
type PersonEvent = StoredEvent<PersonEventDetails>;
|
||||
|
||||
impl PersonEvent {
|
||||
pub fn had_birthday(p: &Person) -> Self {
|
||||
StoredEvent::new(p.id(), p.version, PersonEventDetails::HadBirthday)
|
||||
}
|
||||
|
||||
pub fn name_changed(p: &Person, name: String) -> Self {
|
||||
StoredEvent::new(
|
||||
p.id(),
|
||||
p.version,
|
||||
PersonEventDetails::NameChanged(name))
|
||||
}
|
||||
}
|
||||
|
||||
type PersonCommand = SentCommand<PersonCommandDetails>;
|
||||
|
||||
impl PersonCommand {
|
||||
|
||||
pub fn go_around_sun(id: &AggregateId, version: Option<u64>) -> Self {
|
||||
Self::new(id, version, PersonCommandDetails::GoAroundTheSun)
|
||||
}
|
||||
|
||||
pub fn change_name(id: &AggregateId, version: Option<u64>, s: &str) -> Self {
|
||||
let details = PersonCommandDetails::ChangeName(s.to_string());
|
||||
Self::new(id, version, details)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Display)]
|
||||
enum PersonError {
|
||||
#[display(fmt = "No person can live longer than 255 years")]
|
||||
TooOld
|
||||
}
|
||||
|
||||
impl std::error::Error for PersonError {}
|
||||
|
||||
impl Aggregate for Person {
|
||||
type Command = PersonCommand;
|
||||
type Event = PersonEvent;
|
||||
type InitEvent = InitPersonEvent;
|
||||
type Error = PersonError;
|
||||
|
||||
fn init(event: Self::InitEvent) -> Result<Self, Self::Error> {
|
||||
let (id, _version, init) = event.unwrap();
|
||||
Ok(Person {
|
||||
id, version: 1, name: init.name, age: 0
|
||||
})
|
||||
}
|
||||
|
||||
fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Self::Event) {
|
||||
match event.into_details() {
|
||||
PersonEventDetails::NameChanged(name) => { self.name = name },
|
||||
PersonEventDetails::HadBirthday => { self.age = self.age + 1 }
|
||||
}
|
||||
self.version = self.version + 1;
|
||||
}
|
||||
|
||||
fn process_command(&self, command: Self::Command) -> Result<Vec<Self::Event>, Self::Error> {
|
||||
match command.into_details() {
|
||||
PersonCommandDetails::ChangeName(name) => {
|
||||
let event = PersonEvent::name_changed(&self, name);
|
||||
Ok(vec![event])
|
||||
},
|
||||
PersonCommandDetails::GoAroundTheSun => {
|
||||
if self.age == 255 {
|
||||
Err(PersonError::TooOld)
|
||||
} else {
|
||||
let event = PersonEvent::had_birthday(&self);
|
||||
Ok(vec![event])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PersonManager = AggregateManager<Person, DiskKeyStore>;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
test::test_with_tmp_dir(|d| {
|
||||
|
||||
let storage = DiskKeyStore::new(d.clone());
|
||||
let manager = PersonManager::new(storage);
|
||||
|
||||
let id_alice = AggregateId::new("alice");
|
||||
let alice_init = InitPersonEvent::init(&id_alice, "alice smith");
|
||||
|
||||
manager.create(&id_alice, alice_init).unwrap();
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith");
|
||||
assert_eq!(alice.age(), 0);
|
||||
|
||||
let mut age = 0;
|
||||
loop {
|
||||
manager.apply(
|
||||
PersonCommand::go_around_sun(&id_alice, None)
|
||||
).unwrap();
|
||||
age = age + 1;
|
||||
if age == 21 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith");
|
||||
assert_eq!(alice.age(), 21);
|
||||
|
||||
manager.apply(
|
||||
PersonCommand::change_name(&id_alice, Some(22), "alice smith-doe")
|
||||
).unwrap();
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith-doe");
|
||||
assert_eq!(alice.age(), 21);
|
||||
|
||||
// Should read state from disk
|
||||
let storage = DiskKeyStore::new(d);
|
||||
let manager = PersonManager::new(storage);
|
||||
|
||||
let alice = manager.get_latest(&id_alice).unwrap().unwrap();
|
||||
assert_eq!(alice.name(), "alice smith-doe");
|
||||
assert_eq!(alice.age(), 21);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
//! Storing values for future use
|
||||
pub mod caching_ks;
|
||||
pub mod events;
|
||||
pub mod keystore;
|
||||
|
||||
@@ -116,4 +116,3 @@ pub fn de_facility<'de, D>(d: D) -> Result<Facility, D::Error>
|
||||
|_| { de::Error::custom(
|
||||
format!("Unsupported syslog_facility: \"{}\"", string))})
|
||||
}
|
||||
|
||||
|
||||
+50
-35
@@ -4,13 +4,11 @@ use std::io::{self, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use bytes::Bytes;
|
||||
use rpki::uri;
|
||||
use crate::api::publication;
|
||||
use crate::api::{ Base64, EncodedHash };
|
||||
use crate::api::publication_data;
|
||||
use crate::util::ext_serde;
|
||||
use crate::util::sha256;
|
||||
|
||||
|
||||
///-- Some helper functions
|
||||
|
||||
/// Creates a sub dir if needed, return full path to it
|
||||
pub fn sub_dir(base: &PathBuf, name: &str) -> Result<PathBuf, io::Error> {
|
||||
let mut full_path = base.clone();
|
||||
@@ -106,12 +104,31 @@ pub fn delete_in_dir(
|
||||
delete(&full_path)
|
||||
}
|
||||
|
||||
fn delete(full_path: &PathBuf) -> Result<(), io::Error> {
|
||||
pub fn delete(full_path: &PathBuf) -> Result<(), io::Error> {
|
||||
trace!("Removing file: {}", full_path.to_string_lossy());
|
||||
fs::remove_file(full_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clean_file_and_path(path: &PathBuf) -> Result<(), io::Error> {
|
||||
if path.exists() {
|
||||
fs::remove_file(&path)?;
|
||||
|
||||
let mut parent_opt = path.parent();
|
||||
|
||||
while parent_opt.is_some() {
|
||||
let parent = parent_opt.unwrap();
|
||||
if parent.read_dir()?.count() == 0 {
|
||||
debug!("Will delete {}", parent.to_string_lossy().to_string());
|
||||
fs::remove_dir(parent)?;
|
||||
}
|
||||
|
||||
parent_opt = parent.parent();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_with_rsync(base_path: &PathBuf, uri: &uri::Rsync) -> PathBuf {
|
||||
let mut path = base_path.clone();
|
||||
path.push(uri.module().authority());
|
||||
@@ -158,7 +175,7 @@ fn crawl_disk(
|
||||
} else {
|
||||
let uri = derive_uri(base_path, &path, rsync_base)?;
|
||||
let content = read(&path)?;
|
||||
let current_file = CurrentFile::new(uri, content);
|
||||
let current_file = CurrentFile::new(uri, &content);
|
||||
|
||||
res.push(current_file);
|
||||
}
|
||||
@@ -198,72 +215,71 @@ pub struct CurrentFile {
|
||||
serialize_with = "ext_serde::ser_rsync_uri")]
|
||||
uri: uri::Rsync,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
/// The actual file content. Note that we may want to store this
|
||||
/// only on disk in future (look up by sha256 hash), to save memory.
|
||||
content: Bytes,
|
||||
content: Base64,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "ext_serde::de_bytes",
|
||||
serialize_with = "ext_serde::ser_bytes")]
|
||||
/// The sha-256 hash of the file (as is used on the RPKI manifests and
|
||||
/// in the publication protocol for list, update and withdraw). Saving
|
||||
/// this rather than calculating on demand seems a small price for some
|
||||
/// performance gain.
|
||||
hash: Bytes
|
||||
hash: EncodedHash
|
||||
}
|
||||
|
||||
|
||||
impl CurrentFile {
|
||||
pub fn new(uri: uri::Rsync, content: Bytes) -> Self {
|
||||
let hash = sha256(&content);
|
||||
pub fn new(uri: uri::Rsync, content: &Bytes) -> Self {
|
||||
let content = Base64::from_content(&content);
|
||||
let hash = content.to_encoded_hash();
|
||||
CurrentFile {uri, content, hash}
|
||||
}
|
||||
|
||||
/// Saves this file under a base directory, based on the (rsync) uri of
|
||||
/// this file.
|
||||
pub fn save(&self, base_path: &PathBuf) -> Result<(), io::Error> {
|
||||
save_with_rsync_uri(&self.content, &base_path, &self.uri)
|
||||
save_with_rsync_uri(&self.content.to_bytes(), &base_path, &self.uri)
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &uri::Rsync {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &Bytes {
|
||||
pub fn content(&self) -> &Base64 {
|
||||
&self.content
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &Bytes {
|
||||
pub fn to_bytes(&self) -> Bytes {
|
||||
self.content.to_bytes()
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &EncodedHash {
|
||||
&self.hash
|
||||
}
|
||||
|
||||
pub fn as_publish(&self) -> publication::Publish {
|
||||
let tag = Some(hex::encode(&self.hash));
|
||||
pub fn as_publish(&self) -> publication_data::Publish {
|
||||
let tag = Some(self.hash.to_string());
|
||||
let uri = self.uri.clone();
|
||||
let content = self.content.clone();
|
||||
publication::Publish::new(tag, uri, content)
|
||||
publication_data::Publish::new(tag, uri, content)
|
||||
}
|
||||
|
||||
pub fn as_update(&self, old_hash: &Bytes) -> publication::Update {
|
||||
pub fn as_update(&self, old_hash: &EncodedHash) -> publication_data::Update {
|
||||
let tag = None;
|
||||
let uri = self.uri.clone();
|
||||
let content = self.content.clone();
|
||||
let hash = old_hash.clone();
|
||||
publication::Update::new(tag, uri, content, hash)
|
||||
publication_data::Update::new(tag, uri, content, hash)
|
||||
}
|
||||
|
||||
pub fn as_withdraw(&self) -> publication::Withdraw {
|
||||
pub fn as_withdraw(&self) -> publication_data::Withdraw {
|
||||
let tag = None;
|
||||
let uri = self.uri.clone();
|
||||
let hash = sha256(&self.content);
|
||||
publication::Withdraw::new(tag, uri, hash)
|
||||
let hash = self.hash.clone();
|
||||
publication_data::Withdraw::new(tag, uri, hash)
|
||||
}
|
||||
|
||||
pub fn into_list_element(self) -> publication::ListElement {
|
||||
publication::ListElement::new(self.uri, self.hash)
|
||||
pub fn into_list_element(self) -> publication_data::ListElement {
|
||||
publication_data::ListElement::new(self.uri, self.hash)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -321,20 +337,19 @@ mod tests {
|
||||
|
||||
let file_1 = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/alice/file1.txt"),
|
||||
Bytes::from("content 1")
|
||||
&Bytes::from("content 1")
|
||||
);
|
||||
let file_2 = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/alice/file2.txt"),
|
||||
Bytes::from("content 2")
|
||||
&Bytes::from("content 2")
|
||||
);
|
||||
let file_3 = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/alice/sub/file1\
|
||||
.txt"),
|
||||
Bytes::from("content sub file")
|
||||
test::rsync_uri("rsync://host:10873/module/alice/sub/file1.txt"),
|
||||
&Bytes::from("content sub file")
|
||||
);
|
||||
let file_4 = CurrentFile::new(
|
||||
test::rsync_uri("rsync://host:10873/module/bob/file.txt"),
|
||||
Bytes::from("content")
|
||||
&Bytes::from("content")
|
||||
);
|
||||
|
||||
file_1.save(&base_dir).unwrap();
|
||||
|
||||
+50
-3
@@ -1,6 +1,14 @@
|
||||
//! General utility modules for use all over the code base
|
||||
use std::time::Duration;
|
||||
use bytes::Bytes;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use chrono::offset::TimeZone;
|
||||
use rpki::crypto::DigestAlgorithm;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use serde::Deserializer;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod ext_serde;
|
||||
pub mod file;
|
||||
@@ -9,6 +17,45 @@ pub mod softsigner;
|
||||
pub mod test;
|
||||
pub mod xml;
|
||||
|
||||
pub fn sha256(object: &Bytes) -> Bytes {
|
||||
Bytes::from(DigestAlgorithm.digest(object.as_ref()).as_ref())
|
||||
}
|
||||
pub fn sha256(object: &[u8]) -> Bytes {
|
||||
Bytes::from(DigestAlgorithm.digest(object).as_ref())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Time(DateTime<Utc>);
|
||||
|
||||
impl Time {
|
||||
pub fn now() -> Self {
|
||||
Time(Utc::now())
|
||||
}
|
||||
|
||||
pub fn before_now(dur: Duration) -> Self {
|
||||
let mut millis = Utc::now().timestamp_millis();
|
||||
millis -= dur.as_secs() as i64 * 1000;
|
||||
|
||||
Time(Utc.timestamp_millis(millis))
|
||||
}
|
||||
|
||||
pub fn on_or_before(&self, other: &Time) -> bool {
|
||||
self.0.timestamp_millis() <= other.0.timestamp_millis()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Time {
|
||||
fn serialize<S>(
|
||||
&self, serializer: S
|
||||
) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
serializer.serialize_i64(self.0.timestamp_millis())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Time {
|
||||
fn deserialize<D>(
|
||||
deserializer: D
|
||||
) -> Result<Self, D::Error> where D: Deserializer<'de> {
|
||||
|
||||
let timestamp: i64 = i64::deserialize(deserializer)?;
|
||||
Ok(Time(Utc.timestamp_millis(timestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ extern crate tokio;
|
||||
extern crate bytes;
|
||||
|
||||
use std::{thread, time};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use actix::System;
|
||||
use krill::krillc::data::ReportFormat;
|
||||
@@ -25,7 +26,6 @@ use krill::pubc::apiclient;
|
||||
use krill::pubc::apiclient::ApiResponse;
|
||||
use krill::util::file::CurrentFile;
|
||||
use krill::util::file;
|
||||
use std::collections::HashSet;
|
||||
use krill::util::httpclient;
|
||||
|
||||
fn list(server_uri: &str, handle: &str, token: &str) -> apiclient::Options {
|
||||
@@ -126,6 +126,7 @@ fn client_publish_at_server() {
|
||||
match res {
|
||||
Err(apiclient::Error::HttpClientError
|
||||
(httpclient::Error::Forbidden)) => {},
|
||||
Err(e) => panic!("Expected forbidden, got: {}", e),
|
||||
_ => panic!("Expected forbidden")
|
||||
}
|
||||
}
|
||||
@@ -146,24 +147,25 @@ fn client_publish_at_server() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create files on disk to sync
|
||||
let sync_dir = test::create_sub_dir(&d);
|
||||
let file_a = CurrentFile::new(
|
||||
test::rsync_uri("rsync://127.0.0.1/repo/alice/a.txt"),
|
||||
test::as_bytes("a")
|
||||
&test::as_bytes("a")
|
||||
);
|
||||
let file_b = CurrentFile::new(
|
||||
test::rsync_uri("rsync://127.0.0.1/repo/alice/b.txt"),
|
||||
test::as_bytes("b")
|
||||
&test::as_bytes("b")
|
||||
);
|
||||
let file_c = CurrentFile::new(
|
||||
test::rsync_uri("rsync://127.0.0.1/repo/alice/c.txt"),
|
||||
test::as_bytes("c")
|
||||
&test::as_bytes("c")
|
||||
);
|
||||
|
||||
file::save_in_dir(file_a.content(), &sync_dir, "a.txt").unwrap();
|
||||
file::save_in_dir(file_b.content(), &sync_dir, "b.txt").unwrap();
|
||||
file::save_in_dir(file_c.content(), &sync_dir, "c.txt").unwrap();
|
||||
file::save_in_dir(&file_a.to_bytes(), &sync_dir, "a.txt").unwrap();
|
||||
file::save_in_dir(&file_b.to_bytes(), &sync_dir, "b.txt").unwrap();
|
||||
file::save_in_dir(&file_c.to_bytes(), &sync_dir, "c.txt").unwrap();
|
||||
|
||||
|
||||
// Must refuse syncing files outside of publisher base dir
|
||||
@@ -273,10 +275,6 @@ fn client_publish_at_server() {
|
||||
// XXX TODO Must remove files when removing publisher
|
||||
// Expect that c.txt is removed when looking at latest snapshot.
|
||||
file::delete_in_dir(&sync_dir, "c.txt").unwrap();
|
||||
|
||||
|
||||
// Re-add publisher
|
||||
add_publisher(handle, base_rsync_uri_alice, token);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -95,20 +95,20 @@ fn client_publish_at_server() {
|
||||
let sync_dir = test::create_sub_dir(&d);
|
||||
let file_a = CurrentFile::new(
|
||||
test::rsync_uri("rsync://127.0.0.1/repo/alice/a.txt"),
|
||||
test::as_bytes("a")
|
||||
&test::as_bytes("a")
|
||||
);
|
||||
let file_b = CurrentFile::new(
|
||||
test::rsync_uri("rsync://127.0.0.1/repo/alice/b.txt"),
|
||||
test::as_bytes("b")
|
||||
&test::as_bytes("b")
|
||||
);
|
||||
let file_c = CurrentFile::new(
|
||||
test::rsync_uri("rsync://127.0.0.1/repo/alice/c.txt"),
|
||||
test::as_bytes("c")
|
||||
&test::as_bytes("c")
|
||||
);
|
||||
|
||||
file::save_in_dir(file_a.content(), &sync_dir, "a.txt").unwrap();
|
||||
file::save_in_dir(file_b.content(), &sync_dir, "b.txt").unwrap();
|
||||
file::save_in_dir(file_c.content(), &sync_dir, "c.txt").unwrap();
|
||||
file::save_in_dir(&file_a.to_bytes(), &sync_dir, "a.txt").unwrap();
|
||||
file::save_in_dir(&file_b.to_bytes(), &sync_dir, "b.txt").unwrap();
|
||||
file::save_in_dir(&file_c.to_bytes(), &sync_dir, "c.txt").unwrap();
|
||||
|
||||
client.sync_dir(&sync_dir).unwrap();
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ use krill::krillc::data::{
|
||||
ApiResponse,
|
||||
ReportFormat
|
||||
};
|
||||
use krill::krillc;
|
||||
use krill::krillc::KrillClient;
|
||||
use krill::krillc::options::{
|
||||
AddPublisherWithCms,
|
||||
@@ -25,9 +24,8 @@ use krill::krillc::options::{
|
||||
};
|
||||
use krill::pubc::cmsclient::PubClient;
|
||||
use krill::util::test;
|
||||
use krill::util::httpclient;
|
||||
use krill::remote::rfc8183::RepositoryResponse;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
|
||||
/// Tests that we can list publishers through the API
|
||||
#[test]
|
||||
@@ -123,7 +121,7 @@ fn manage_publishers() {
|
||||
match res {
|
||||
ApiResponse::PublisherDetails(details) => {
|
||||
assert_eq!(
|
||||
details.publisher_handle(),
|
||||
details.handle(),
|
||||
"alice"
|
||||
);
|
||||
}
|
||||
@@ -177,17 +175,15 @@ fn manage_publishers() {
|
||||
Command::Publishers(PublishersCommand::Details("alice".to_string()))
|
||||
);
|
||||
|
||||
let res = KrillClient::process(krillc_opts);
|
||||
let res = KrillClient::process(krillc_opts).unwrap();
|
||||
|
||||
match res {
|
||||
Err(krillc::Error::HttpClientError(
|
||||
httpclient::Error::BadStatus(code))) => {
|
||||
assert_eq!(code, StatusCode::NOT_FOUND);
|
||||
},
|
||||
_ => assert!(false) // should have failed!
|
||||
ApiResponse::PublisherDetails(details) => {
|
||||
assert_eq!(details.handle(), "alice");
|
||||
assert!(details.retired());
|
||||
}
|
||||
_ => panic!("Expected to find alice in retired state")
|
||||
}
|
||||
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user