mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-10-01 13:54:53 +02:00
449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
//! An RPKI publication protocol server.
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use bcder::Captured;
|
|
use provisioning::publisher::Publisher;
|
|
use provisioning::publisher_store::{self, PublisherStore};
|
|
use pubd::responder::{self, Responder};
|
|
use repo::file_store;
|
|
use repo::repository::{self, Repository};
|
|
use rpki::oob::exchange::RepositoryResponse;
|
|
use rpki::publication::pubmsg::{Message, MessageError, QueryMessage};
|
|
use rpki::publication::reply::{ErrorReply, ReportError, ReportErrorCode};
|
|
use rpki::remote::sigmsg::SignedMessage;
|
|
use rpki::uri;
|
|
use rpki::x509::ValidationError;
|
|
|
|
|
|
/// # Naming things in the keystore.
|
|
fn actor() -> String {
|
|
"publication server".to_string()
|
|
}
|
|
|
|
|
|
//------------ PubServer -----------------------------------------------------
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PubServer {
|
|
// The component that manages server id, and wraps responses to clients
|
|
responder: Responder,
|
|
|
|
// The configured publishers
|
|
publisher_store: PublisherStore,
|
|
|
|
// The repository responsible for publishing rsync and rrdp
|
|
repository: Repository,
|
|
}
|
|
|
|
/// # Set up and initialisation
|
|
impl PubServer {
|
|
/// Creates a new publication server. Note that state is preserved
|
|
/// on disk in the work_dir provided.
|
|
pub fn new(
|
|
work_dir: &PathBuf,
|
|
pub_xml_dir: &PathBuf,
|
|
base_uri: &uri::Rsync,
|
|
service_uri: uri::Http,
|
|
rrdp_notification_uri: uri::Http
|
|
) -> Result<Self, Error> {
|
|
let responder = Responder::init(
|
|
work_dir,
|
|
service_uri,
|
|
rrdp_notification_uri)?;
|
|
|
|
let publisher_store = Self::init_publishers(
|
|
&work_dir,
|
|
pub_xml_dir,
|
|
base_uri)?;
|
|
|
|
let repository = Repository::new(work_dir)?;
|
|
|
|
Ok(
|
|
PubServer {
|
|
responder,
|
|
repository,
|
|
publisher_store
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
/// # Configure publishers
|
|
impl PubServer {
|
|
/// Synchronize publishers from disk
|
|
fn init_publishers(
|
|
work_dir: &PathBuf,
|
|
pub_xml_dir: &PathBuf,
|
|
base_uri: &uri::Rsync
|
|
) -> Result<PublisherStore, Error> {
|
|
let mut publisher_store = PublisherStore::new(
|
|
work_dir,
|
|
base_uri)?;
|
|
publisher_store.sync_from_dir(pub_xml_dir, actor())?;
|
|
Ok(publisher_store)
|
|
}
|
|
|
|
/// Returns all currently configured publishers.
|
|
pub fn publishers(&self) -> Result<Vec<Arc<Publisher>>, Error> {
|
|
self.publisher_store
|
|
.publishers()
|
|
.map_err(|e| { Error::PublisherStoreError(e) })
|
|
}
|
|
|
|
/// Returns a repository response for the given publisher.
|
|
///
|
|
/// Returns an error if the publisher is unknown.
|
|
pub fn repository_response(
|
|
&self,
|
|
publisher_name: &str
|
|
) -> Result<RepositoryResponse, Error> {
|
|
let publisher = self.publisher_store.get_publisher(publisher_name)?;
|
|
self.responder
|
|
.repository_response(publisher)
|
|
.map_err(|e| Error::ResponderError(e))
|
|
}
|
|
}
|
|
|
|
/// # Handle publisher requests
|
|
///
|
|
impl PubServer {
|
|
|
|
/// Handles an incoming SignedMessage, verifies it's validly signed by
|
|
/// a known publisher and process the QueryMessage contained. Returns
|
|
/// a signed response to the publisher.
|
|
///
|
|
/// Note this returns an error for cases where we do not want to do any
|
|
/// work in signing, like the publisher does not exist, or the
|
|
/// signature is invalid. The daemon will need to map these to HTTTP
|
|
/// codes.
|
|
///
|
|
/// Also note that if garbage is sent to the daemon, this garbage will
|
|
/// fail to parse as a SignedMessage, and the daemon will just respond
|
|
/// with an HTTP error response, without invoking any of this.
|
|
pub fn handle_request(
|
|
&mut self,
|
|
sigmsg: SignedMessage,
|
|
publisher_handle: &str
|
|
) -> Result<Captured, Error> {
|
|
let publisher = self.publisher_store.get_publisher(publisher_handle)?;
|
|
let base_uri = publisher.base_uri();
|
|
sigmsg.validate(publisher.id_cert())?;
|
|
|
|
let res_msg = match Message::from_signed_message(&sigmsg) {
|
|
Ok(msg) => {
|
|
match msg.as_query() {
|
|
Ok(query) => self.handle_query(&query, base_uri),
|
|
Err(e) => Self::build_error(e)
|
|
}
|
|
},
|
|
Err(e) => {
|
|
Self::build_error(e)
|
|
}
|
|
};
|
|
|
|
let sigres = self.responder.sign_msg(res_msg)?;
|
|
Ok(sigres)
|
|
}
|
|
|
|
|
|
/// Handles a publish or list query for a publisher. Needs to know the
|
|
/// base_uri for the publisher to enforce constraints, but can assume
|
|
/// that the PubServer has already validated the incoming SignedMessage.
|
|
///
|
|
/// Returns the appropriate Success or Error Reply in a Message, ready
|
|
/// for wrapping into a SignedMessage.
|
|
fn handle_query(
|
|
&self,
|
|
query: &QueryMessage,
|
|
base_uri: &uri::Rsync
|
|
) -> Message {
|
|
match query {
|
|
QueryMessage::PublishQuery(publish) => {
|
|
match self.repository.publish(publish, base_uri) {
|
|
Err(e) => {
|
|
Self::build_error(e)
|
|
},
|
|
Ok(success) => success
|
|
}
|
|
},
|
|
QueryMessage::ListQuery(_list) => {
|
|
match self.repository.list(base_uri) {
|
|
Err(e) => {
|
|
Self::build_error(e)
|
|
},
|
|
Ok(success) => success
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_error(error: impl ToReportErrorCode) -> Message {
|
|
let mut error_builder = ErrorReply::build();
|
|
error_builder.add(
|
|
ReportError::reply(
|
|
error.to_report_error_code(),
|
|
None // Finding the specific PDU is too much hard work.
|
|
)
|
|
);
|
|
error_builder.build_message()
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------ Error ---------------------------------------------------------
|
|
|
|
#[derive(Debug, Fail)]
|
|
pub enum Error {
|
|
#[fail(display="{}", _0)]
|
|
ResponderError(responder::Error),
|
|
|
|
#[fail(display="{}", _0)]
|
|
PublisherStoreError(publisher_store::Error),
|
|
|
|
#[fail(display="{}", _0)]
|
|
RepositoryError(repository::Error),
|
|
|
|
#[fail(display="{}", _0)]
|
|
MessageError(MessageError),
|
|
|
|
#[fail(display="{}", _0)]
|
|
ValdiationError(ValidationError),
|
|
}
|
|
|
|
impl From<responder::Error> for Error {
|
|
fn from(e: responder::Error) -> Self {
|
|
Error::ResponderError(e)
|
|
}
|
|
}
|
|
|
|
impl From<publisher_store::Error> for Error {
|
|
fn from(e: publisher_store::Error) -> Self {
|
|
Error::PublisherStoreError(e)
|
|
}
|
|
}
|
|
|
|
impl From<repository::Error> for Error {
|
|
fn from(e: repository::Error) -> Self {
|
|
Error::RepositoryError(e)
|
|
}
|
|
}
|
|
|
|
impl From<MessageError> for Error {
|
|
fn from(e: MessageError) -> Self {
|
|
Error::MessageError(e)
|
|
}
|
|
}
|
|
|
|
impl From<ValidationError> for Error {
|
|
fn from(e: ValidationError) -> Self {
|
|
Error::ValdiationError(e)
|
|
}
|
|
}
|
|
|
|
|
|
//------------ ToReportErrorCode ---------------------------------------------
|
|
|
|
trait ToReportErrorCode {
|
|
fn to_report_error_code(&self) -> ReportErrorCode;
|
|
}
|
|
|
|
impl ToReportErrorCode for MessageError {
|
|
fn to_report_error_code(&self) -> ReportErrorCode {
|
|
ReportErrorCode::XmlError
|
|
}
|
|
}
|
|
|
|
impl ToReportErrorCode for repository::Error {
|
|
fn to_report_error_code(&self) -> ReportErrorCode {
|
|
match self {
|
|
repository::Error::FileStoreError(error) =>
|
|
error.to_report_error_code()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToReportErrorCode for file_store::Error {
|
|
fn to_report_error_code(&self) -> ReportErrorCode {
|
|
match self {
|
|
file_store::Error::ObjectAlreadyPresent(_) =>
|
|
ReportErrorCode::ObjectAlreadyPresent,
|
|
file_store::Error::NoObjectPresent(_) =>
|
|
ReportErrorCode::NoObjectPresent,
|
|
file_store::Error::NoObjectMatchingHash =>
|
|
ReportErrorCode::NoObjectMatchingHash,
|
|
file_store::Error::OutsideBaseUri =>
|
|
ReportErrorCode::PermissionFailure,
|
|
_ => ReportErrorCode::OtherError
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//------------ Tests ---------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use test;
|
|
use pubc::client::PubClient;
|
|
|
|
fn test_server(work_dir: &PathBuf, xml_dir: &PathBuf) -> PubServer {
|
|
// Start up a server
|
|
let uri = test::rsync_uri("rsync://host/module/");
|
|
let service = test::http_uri("http://host/publish");
|
|
let notify = test::http_uri("http://host/notify.xml");
|
|
PubServer::new(
|
|
work_dir,
|
|
xml_dir,
|
|
&uri,
|
|
service,
|
|
notify
|
|
).unwrap()
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
fn should_sync_and_resync_publishers_from_disk() {
|
|
test::test_with_tmp_dir(|d| {
|
|
// Set up an xml dir with two requests
|
|
let xml_dir = test::create_sub_dir(&d);
|
|
|
|
let alice_dir = test::create_sub_dir(&d);
|
|
let mut alice = PubClient::new(&alice_dir).unwrap();
|
|
alice.init("alice".to_string()).unwrap();
|
|
let pr_alice = alice.publisher_request().unwrap();
|
|
|
|
let bob_dir = test::create_sub_dir(&d);
|
|
let mut bob = PubClient::new(&bob_dir).unwrap();
|
|
bob.init("bob".to_string()).unwrap();
|
|
let pr_bob = bob.publisher_request().unwrap();
|
|
|
|
test::save_file(&xml_dir, "alice.xml", &pr_alice.encode_vec());
|
|
test::save_file(&xml_dir, "bob.xml", &pr_bob.encode_vec());
|
|
|
|
// Start up a server
|
|
let server = test_server(&d, &xml_dir);
|
|
|
|
// The server now has two configured publishers
|
|
let publishers = server.publishers().unwrap();
|
|
assert_eq!(2, publishers.len());
|
|
|
|
// Create a new xml dir with only alice.xml
|
|
let xml_dir = PathBuf::from(test::create_sub_dir(&d));
|
|
test::save_file(&xml_dir, "alice.xml", &pr_alice.encode_vec());
|
|
|
|
// Start a new server (so that it re-syncs)
|
|
let server = test_server(&d, &xml_dir);
|
|
|
|
// Now we expect only one publisher for Alice
|
|
let publishers = server.publishers().unwrap();
|
|
assert_eq!(1, publishers.len());
|
|
let p_alice = publishers
|
|
.iter()
|
|
.find(|p| { p.name() == "alice" })
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
pr_alice.id_cert().to_bytes(),
|
|
p_alice.id_cert().to_bytes()
|
|
);
|
|
|
|
let p_old_alice = p_alice;
|
|
|
|
// But we can update Alice's id cert, and add carol
|
|
alice.init("alice".to_string()).unwrap();
|
|
let pr_alice = alice.publisher_request().unwrap();
|
|
|
|
let carol_dir = test::create_sub_dir(&d);
|
|
let mut carol = PubClient::new(&carol_dir).unwrap();
|
|
carol.init("carol".to_string()).unwrap();
|
|
let pr_carol = carol.publisher_request().unwrap();
|
|
|
|
test::save_file(&xml_dir, "alice.xml", &pr_alice.encode_vec());
|
|
test::save_file(&xml_dir, "carol.xml", &pr_carol.encode_vec());
|
|
|
|
let server = test_server(&d, &xml_dir);
|
|
|
|
// Now we expect a different Alice and Carol
|
|
let publishers = server.publishers().unwrap();
|
|
assert_eq!(2, publishers.len());
|
|
|
|
let p_alice = publishers
|
|
.iter()
|
|
.find(|p| { p.name() == "alice" })
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
pr_alice.id_cert().to_bytes(),
|
|
p_alice.id_cert().to_bytes()
|
|
);
|
|
|
|
assert_ne!(
|
|
p_old_alice.id_cert().to_bytes(),
|
|
p_alice.id_cert().to_bytes()
|
|
);
|
|
|
|
// Prove that we also have carol
|
|
publishers.iter().find(|p| { p.name() == "carol" }).unwrap();
|
|
|
|
// However, initialising the server with two or more xml files
|
|
// for the same handle results in an error.
|
|
test::save_file(&xml_dir, "alice-2.xml", &pr_alice.encode_vec());
|
|
|
|
let uri = test::rsync_uri("rsync://host/module/");
|
|
let service = test::http_uri("http://host/publish");
|
|
let notify = test::http_uri("http://host/notify.xml");
|
|
|
|
assert!(
|
|
PubServer::new(
|
|
&d,
|
|
&xml_dir,
|
|
&uri,
|
|
service,
|
|
notify
|
|
).is_err()
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn should_initialise_publishers_from_xml_and_have_response() {
|
|
test::test_with_tmp_dir(|d| {
|
|
let xml_dir = test::create_sub_dir(&d);
|
|
|
|
let alice_dir = test::create_sub_dir(&d);
|
|
let mut alice = PubClient::new(&alice_dir).unwrap();
|
|
alice.init("alice".to_string()).unwrap();
|
|
let pr_alice = alice.publisher_request().unwrap();
|
|
|
|
let bob_dir = test::create_sub_dir(&d);
|
|
let mut bob = PubClient::new(&bob_dir).unwrap();
|
|
bob.init("bob".to_string()).unwrap();
|
|
let pr_bob = bob.publisher_request().unwrap();
|
|
|
|
test::save_file(&xml_dir, "alice.xml", &pr_alice.encode_vec());
|
|
test::save_file(&xml_dir, "bob.xml", &pr_bob.encode_vec());
|
|
|
|
let server = test_server(&d, &xml_dir);
|
|
|
|
let response = server.repository_response("alice").unwrap();
|
|
|
|
let expected_sia = test::rsync_uri("rsync://host/module/alice/");
|
|
let expected_service = test::http_uri("http://host/publish");
|
|
let expected_notify = test::http_uri("http://host/notify.xml");
|
|
|
|
assert_eq!(&expected_sia, response.sia_base());
|
|
assert_eq!(&expected_service, response.service_uri());
|
|
assert_eq!(&expected_notify, response.rrdp_notification_uri());
|
|
assert_eq!("alice", response.publisher_handle());
|
|
});
|
|
}
|
|
|
|
}
|
|
|