Files
NLnetLabs-krill/pubd/src/pubserver.rs
T

622 lines
21 KiB
Rust

use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use chrono::Duration;
use rpki::uri;
use krill_commons::api::admin::{Handle, PublisherRequest};
use krill_commons::api::ca::RepoInfo;
use krill_commons::api::publication;
use krill_commons::api::rrdp::DeltaElements;
use krill_commons::eventsourcing::{
Aggregate, AggregateStore, AggregateStoreError, Command, DiskAggregateStore,
};
use crate::publishers::{
InitPublisherDetails, Publisher, PublisherCommand, PublisherCommandDetails, PublisherError,
PublisherEventDetails,
};
use crate::repo::{
self, RrdpCommandDetails, RrdpInitDetails, RrdpServer, RrdpServerError, RsyncdStore,
};
//------------ PubServer -----------------------------------------------------
/// This server manages all publishers. I.e. finds them, adds them, dispatches
/// commands to them, stores them.. also publishes the combined snapshots and
/// deltas, and manages the files on disk for rsync.
pub struct PubServer {
rrdp_store: Arc<DiskAggregateStore<RrdpServer>>,
rsyncd_store: RsyncdStore,
store: Arc<DiskAggregateStore<Publisher>>,
base_rsync_uri: uri::Rsync, // jail for the publishers,
command_lock: Mutex<()>, // Only one command at the time.
}
impl PubServer {
pub fn build(
base_rsync_uri: uri::Rsync,
base_http_uri: uri::Https, // for the RRDP files
repo_dir: PathBuf, // for the RRDP and rsync files
work_dir: &PathBuf, // for the aggregate stores
) -> Result<Self, Error> {
let rrdp_store = Arc::new(DiskAggregateStore::<RrdpServer>::new(
work_dir,
"repo-server",
)?);
let rsyncd_store = RsyncdStore::build(&repo_dir)?;
if !rrdp_store.has(&repo::id()) {
let init = RrdpInitDetails::init_new(base_http_uri, repo_dir);
rrdp_store.add(init)?;
}
let store = Arc::new(DiskAggregateStore::<Publisher>::new(
work_dir,
"publishers",
)?);
let command_lock = Mutex::new(());
let pubserver = PubServer {
rrdp_store,
rsyncd_store,
store,
base_rsync_uri,
command_lock,
};
Ok(pubserver)
}
pub fn repo_info_for(&self, handle: &Handle) -> Result<RepoInfo, Error> {
let rsync_jail = format!("{}{}/", self.base_rsync_uri.to_string(), handle);
let base_uri = uri::Rsync::from_string(rsync_jail).unwrap();
let rpki_notify = self.rrdp_server()?.notification_uri();
Ok(RepoInfo::new(base_uri, rpki_notify))
}
pub fn ta_aia(&self) -> uri::Rsync {
let uri = format!("{}ta.cer", self.base_rsync_uri.to_string());
uri::Rsync::from_string(uri).unwrap()
}
}
/// # Publication Protocol support
///
impl PubServer {
fn rrdp_server(&self) -> Result<Arc<RrdpServer>, Error> {
self.rrdp_store
.get_latest(&repo::id())
.map_err(Error::AggregateStoreError)
}
pub fn publish(&self, handle: &Handle, delta: publication::PublishDelta) -> Result<(), Error> {
// Only do one update at a time.
let _lock = self.command_lock.lock().unwrap();
// Publish the delta for the publisher
let cmd = PublisherCommandDetails::publish(handle, delta);
if let Some(delta) = self.command_publisher(cmd)? {
// Apparently the delta was valid, and for a known publisher
let repo_id = repo::id();
// Update rsync repo on disk
let delta = delta.clone();
self.rsyncd_store.publish(&delta)?;
// Add the delta to the RRDP server
let rrdp = self.rrdp_server()?;
let add_cmd = RrdpCommandDetails::add_delta(delta);
let rrdp_add_delta_events = rrdp.process_command(add_cmd)?;
let rrdp = self
.rrdp_store
.update(&repo_id, rrdp, rrdp_add_delta_events)?;
// Trigger publication of the RRDP files
let publish_cmd = RrdpCommandDetails::publish();
let rrdp_publish_events = rrdp.process_command(publish_cmd)?;
let rrdp = self
.rrdp_store
.update(&repo_id, rrdp, rrdp_publish_events)?;
// Clean up old files
let retention = Duration::seconds(0);
let clean_cmd = RrdpCommandDetails::clean_up(retention);
let rrdp_clean_events = rrdp.process_command(clean_cmd)?;
self.rrdp_store.update(&repo_id, rrdp, rrdp_clean_events)?;
}
Ok(())
}
pub fn list(&self, handle: &Handle) -> Result<publication::ListReply, Error> {
match self.get_publisher(handle)? {
Some(publisher) => Ok(publisher.list_current()),
None => Err(Error::UnknownPublisher(handle.to_string())),
}
}
}
/// # Publishing
///
impl PubServer {
fn verify_handle(&self, handle: &Handle) -> Result<(), Error> {
let name = handle.as_str();
if !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
return Err(Error::InvalidHandle(name.to_string()));
}
if self.store.has(handle) {
return Err(Error::DuplicatePublisher(name.to_string()));
}
Ok(())
}
fn verify_base_uri(&self, base_uri: &uri::Rsync) -> Result<(), Error> {
if self.base_rsync_uri.is_parent_of(base_uri) && base_uri.ends_with("/") {
// Note it's allowed for multiple publishers to share the same
// base_uri, and for publishers to have a base_uri under another.
// Maybe we will want to change this in future, but for the moment
// this freedom is given to the admin. There are use cases here,
// such as hierarchical rsync, and possibly migrations that appear
// as standard key rolls to an RP.
Ok(())
} else {
Err(Error::InvalidBaseUri)
}
}
pub fn get_publisher(&self, handle: &Handle) -> Result<Option<Arc<Publisher>>, Error> {
if self.store.has(handle) {
self.store
.get_latest(handle)
.map(Some)
.map_err(Error::AggregateStoreError)
} else {
Ok(None)
}
}
/// Adds a publisher. Will complain if a publisher already exists for this
/// handle. Will also verify that the base_uri is allowed.
pub fn create_publisher(&self, req: PublisherRequest) -> Result<(), Error> {
self.verify_handle(req.handle())?;
self.verify_base_uri(req.base_uri())?;
let init = InitPublisherDetails::for_request(req);
self.store.add(init)?;
Ok(())
}
/// Returns a list of publisher handles
pub fn list_publishers(&self) -> Vec<Handle> {
self.store.list()
}
/// Deactivates a publisher. For now this is irreversible, but we may add
/// re-activation in future. Reason is that we never forget the history
/// of the old publisher, and if handles are re-used by different
/// entities that would get confusing.
pub fn deactivate_publisher(&self, handle: &Handle) -> Result<(), Error> {
let cmd = PublisherCommandDetails::deactivate(handle);
self.command_publisher(cmd)?;
Ok(())
}
/// Apply a command to a publisher. If this was a successful publication
/// command, then return the delta so that it can be published by the
/// RRDP server.
fn command_publisher(&self, command: PublisherCommand) -> Result<Option<DeltaElements>, Error> {
let handle = command.handle().clone();
match self.get_publisher(&handle)? {
None => Err(Error::UnknownPublisher(handle.to_string())),
Some(pbl) => {
let mut res = None;
if let Some(version) = command.version() {
if version != pbl.version() {
return Err(Error::ConcurrentModification(version, pbl.version()));
}
}
let events = pbl.process_command(command)?;
for event in &events {
if let PublisherEventDetails::Published(delta) = event.details() {
res = Some(delta.clone())
}
}
self.store.update(&handle, pbl, events)?;
Ok(res)
}
}
}
}
//------------ Error ---------------------------------------------------------
#[derive(Debug, Display)]
pub enum Error {
#[display(fmt = "{}", _0)]
IoError(io::Error),
#[display(
fmt = "The publisher handle may only contain a-ZA-Z0-9 and _. You sent: {}",
_0
)]
InvalidHandle(String),
#[display(
fmt = "Duplicate publisher with name: {} (note: might be de-activated).",
_0
)]
DuplicatePublisher(String),
#[display(fmt = "Unknown publisher with name: {}.", _0)]
UnknownPublisher(String),
#[display(fmt = "Trying to update version: {}, publisher at: {}", _0, _1)]
ConcurrentModification(u64, u64),
#[display(fmt = "Base uri for publisher needs to be under server, and must end with a '/'.")]
InvalidBaseUri,
#[display(fmt = "{}", _0)]
PublisherError(PublisherError),
#[display(fmt = "{}", _0)]
RrdpServerError(RrdpServerError),
#[display(fmt = "{}", _0)]
AggregateStoreError(AggregateStoreError),
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::IoError(e)
}
}
impl From<PublisherError> for Error {
fn from(e: PublisherError) -> Self {
Error::PublisherError(e)
}
}
impl From<RrdpServerError> for Error {
fn from(e: RrdpServerError) -> Self {
Error::RrdpServerError(e)
}
}
impl From<AggregateStoreError> for Error {
fn from(e: AggregateStoreError) -> Self {
Error::AggregateStoreError(e)
}
}
impl std::error::Error for Error {}
//------------ Tests ---------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use krill_commons::api::admin::Token;
use krill_commons::api::publication::PublishDeltaBuilder;
use krill_commons::api::rrdp::VerificationError;
use krill_commons::util::file::CurrentFile;
use krill_commons::util::test;
use std::path::PathBuf;
fn server_base_uri() -> uri::Rsync {
test::rsync("rsync://localhost/repo/")
}
fn server_base_http_uri() -> uri::Https {
test::https("https://localhost/rrdp/")
}
fn make_publisher_req(handle: &str, uri: &str) -> PublisherRequest {
let base_uri = test::rsync(uri);
let handle = Handle::from(handle);
let token = Token::from("secret");
PublisherRequest::new(handle, token, base_uri)
}
fn make_server(work_dir: &PathBuf) -> PubServer {
let mut base_dir = work_dir.clone();
base_dir.push("repo");
PubServer::build(
server_base_uri(),
server_base_http_uri(),
base_dir,
work_dir,
)
.unwrap()
}
#[test]
fn should_add_publisher() {
test::test_under_tmp(|d| {
let publisher_req = make_publisher_req("alice", "rsync://localhost/repo/alice/");
let server = make_server(&d);
server.create_publisher(publisher_req).unwrap();
let handle = Handle::from("alice");
let alice = server.get_publisher(&handle).unwrap().unwrap();
assert_eq!(alice.handle(), &handle);
})
}
#[test]
fn should_refuse_invalid_publisher_handle() {
test::test_under_tmp(|d| {
let publisher_req = make_publisher_req("alice&", "rsync://localhost/repo/alice/");
let server = make_server(&d);
match server.create_publisher(publisher_req) {
Err(Error::InvalidHandle(handle)) => assert_eq!(handle, "alice&".to_string()),
_ => panic!("Expected error"),
}
})
}
#[test]
fn should_refuse_base_uri_not_ending_with_slash() {
test::test_under_tmp(|d| {
let publisher_req = make_publisher_req("alice", "rsync://localhost/repo/alice");
let server = make_server(&d);
match server.create_publisher(publisher_req) {
Err(Error::InvalidBaseUri) => {}
_ => panic!("Expected error"),
}
})
}
#[test]
fn should_refuse_base_uri_outside_of_server_base() {
test::test_under_tmp(|d| {
let publisher_req = make_publisher_req("alice", "rsync://localhost/outside/alice/");
let server = make_server(&d);
match server.create_publisher(publisher_req) {
Err(Error::InvalidBaseUri) => {}
_ => panic!("Expected error"),
}
})
}
#[test]
fn should_not_add_publisher_twice() {
test::test_under_tmp(|d| {
let publisher_req = make_publisher_req("alice", "rsync://localhost/repo/alice/");
let server = make_server(&d);
server.create_publisher(publisher_req.clone()).unwrap();
match server.create_publisher(publisher_req) {
Err(Error::DuplicatePublisher(name)) => assert_eq!(name, "alice".to_string()),
_ => panic!("Expected error"),
}
})
}
#[test]
fn should_remove_publisher() {
test::test_under_tmp(|d| {
let server = make_server(&d);
let handle = Handle::from("alice");
// create publisher
let publisher_req =
make_publisher_req(handle.as_str(), "rsync://localhost/repo/alice/");
server.create_publisher(publisher_req).unwrap();
// expect to see it in the list
let list = server.list_publishers();
assert_eq!(list, vec![handle.clone()]);
// deactivate
let deactivate = PublisherCommandDetails::deactivate(&handle);
server.command_publisher(deactivate).unwrap();
// expect that it is now inactive
let alice = server.get_publisher(&handle).unwrap().unwrap();
assert!(alice.is_deactivated())
})
}
#[test]
fn should_list_files() {
test::test_under_tmp(|d| {
let publisher_req = make_publisher_req("alice", "rsync://localhost/repo/alice/");
let handle = Handle::from("alice");
let server = make_server(&d);
server.create_publisher(publisher_req).unwrap();
let alice = server.get_publisher(&handle).unwrap().unwrap();
let list_reply = alice.list_current();
assert_eq!(0, list_reply.elements().len());
});
}
#[test]
fn should_publish_files() {
test::test_under_tmp(|d| {
// get the file out of a list_reply
fn find_in_reply<'a>(
reply: &'a publication::ListReply,
uri: &uri::Rsync,
) -> Option<&'a publication::ListElement> {
reply.elements().iter().find(|e| e.uri() == uri)
}
let publisher_req = make_publisher_req("alice", "rsync://localhost/repo/alice/");
let handle = Handle::from("alice");
let server = make_server(&d);
server.create_publisher(publisher_req).unwrap();
// Publish a single file
let file1 = CurrentFile::new(
test::rsync("rsync://localhost/repo/alice/file.txt"),
&Bytes::from("example content"),
);
let file2 = CurrentFile::new(
test::rsync("rsync://localhost/repo/alice/file2.txt"),
&Bytes::from("example content 2"),
);
let mut builder = PublishDeltaBuilder::new();
builder.add_publish(file1.as_publish());
builder.add_publish(file2.as_publish());
let delta = builder.finish();
server.publish(&handle, delta).unwrap();
// Two files should now appear in the list
let alice = server.get_publisher(&handle).unwrap().unwrap();
let list_reply = alice.list_current();
assert_eq!(2, list_reply.elements().len());
assert!(find_in_reply(
&list_reply,
&test::rsync("rsync://localhost/repo/alice/file.txt")
)
.is_some());
assert!(find_in_reply(
&list_reply,
&test::rsync("rsync://localhost/repo/alice/file2.txt")
)
.is_some());
// Update
// - update file
// - withdraw file2
// - add file 3
let file1_update = CurrentFile::new(
test::rsync("rsync://localhost/repo/alice/file.txt"),
&Bytes::from("example content - updated"),
);
let file3 = CurrentFile::new(
test::rsync("rsync://localhost/repo/alice/file3.txt"),
&Bytes::from("example content 3"),
);
let mut builder = PublishDeltaBuilder::new();
builder.add_update(file1_update.as_update(file1.hash()));
builder.add_withdraw(file2.as_withdraw());
builder.add_publish(file3.as_publish());
let delta = builder.finish();
server.publish(&handle, delta).unwrap();
// Two files should now appear in the list
let alice = server.get_publisher(&handle).unwrap().unwrap();
let list_reply = alice.list_current();
assert_eq!(2, list_reply.elements().len());
assert!(find_in_reply(
&list_reply,
&test::rsync("rsync://localhost/repo/alice/file.txt")
)
.is_some());
assert_eq!(
find_in_reply(
&list_reply,
&test::rsync("rsync://localhost/repo/alice/file.txt")
)
.unwrap()
.hash(),
file1_update.hash()
);
assert!(find_in_reply(
&list_reply,
&test::rsync("rsync://localhost/repo/alice/file3.txt")
)
.is_some());
// Should reject publish outside of base uri
let file_outside = CurrentFile::new(
test::rsync("rsync://localhost/repo/bob/file.txt"),
&Bytes::from("irrelevant"),
);
let mut builder = PublishDeltaBuilder::new();
builder.add_publish(file_outside.as_publish());
let delta = builder.finish();
match server.publish(&handle, delta) {
Err(Error::PublisherError(PublisherError::VerificationError(
VerificationError::UriOutsideJail(_, _),
))) => {} // ok
_ => panic!("Expected error publishing outside of base uri jail"),
}
// Should reject update of file that does not exist
let file2_update = CurrentFile::new(
test::rsync("rsync://localhost/repo/alice/file2.txt"),
&Bytes::from("example content 2 updated"),
); // file2 was removed
let mut builder = PublishDeltaBuilder::new();
builder.add_update(file2_update.as_update(file2.hash()));
let delta = builder.finish();
match server.publish(&handle, delta) {
Err(Error::PublisherError(PublisherError::VerificationError(
VerificationError::NoObjectForHashAndOrUri(_),
))) => {}
// ok
_ => panic!("Expected error when file for update can't be found"),
}
// should reject withdraw for file that does not exist
let mut builder = PublishDeltaBuilder::new();
builder.add_withdraw(file2.as_withdraw());
let delta = builder.finish();
let cmd = PublisherCommandDetails::publish(&handle, delta);
match server.command_publisher(cmd) {
Err(Error::PublisherError(PublisherError::VerificationError(
VerificationError::NoObjectForHashAndOrUri(_),
))) => {} // ok
_ => panic!("Expected error withdrawing file that does not exist"),
}
// should reject publish for file that does exist
let mut builder = PublishDeltaBuilder::new();
builder.add_publish(file3.as_publish());
let delta = builder.finish();
match server.publish(&handle, delta) {
Err(Error::PublisherError(PublisherError::VerificationError(
VerificationError::ObjectAlreadyPresent(uri),
))) => assert_eq!(uri, test::rsync("rsync://localhost/repo/alice/file3.txt")),
_ => panic!("Expected error publishing file that already exists"),
}
});
}
}