502 lines
17 KiB
Rust
502 lines
17 KiB
Rust
// RRDP test group: sync.
|
|
|
|
#[test]
|
|
fn snapshot_entity_reference_is_not_silently_dropped_from_base64() {
|
|
for content in ["YQ==&", "YQ==", "YQ==&unknown;"] {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let store = RocksStore::open(temp.path()).unwrap();
|
|
let sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
|
|
let xml = format!(
|
|
"<snapshot xmlns=\"{RRDP_XMLNS}\" version=\"1\" session_id=\"{sid}\" serial=\"1\"><publish uri=\"rsync://example.net/repo/a.roa\">{content}</publish></snapshot>"
|
|
);
|
|
let error = super::snapshot_apply::apply_snapshot(
|
|
&store,
|
|
"https://example.net/notification.xml",
|
|
None,
|
|
xml.as_bytes(),
|
|
sid,
|
|
1,
|
|
)
|
|
.expect_err("an entity must not disappear and turn invalid base64 into valid data");
|
|
assert!(matches!(
|
|
error,
|
|
RrdpSyncError::Rrdp(RrdpError::PublishBase64(_))
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sync_from_notification_snapshot_rejects_cross_source_owner_conflict() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
|
|
|
|
let sid_a = "550e8400-e29b-41d4-a716-446655440000";
|
|
let sid_b = "550e8400-e29b-41d4-a716-446655440001";
|
|
let uri = "rsync://example.net/repo/a.mft";
|
|
|
|
let notif_a_uri = "https://example.net/a/notification.xml";
|
|
let snapshot_a_uri = "https://example.net/a/snapshot.xml";
|
|
let snapshot_a = snapshot_xml(sid_a, 1, &[(uri, b"a1")]);
|
|
let snapshot_a_hash = hex::encode(sha2::Sha256::digest(&snapshot_a));
|
|
let notif_a = notification_xml(sid_a, 1, snapshot_a_uri, &snapshot_a_hash);
|
|
let fetcher_a = MapFetcher {
|
|
map: HashMap::from([(snapshot_a_uri.to_string(), snapshot_a)]),
|
|
};
|
|
sync_from_notification_snapshot(&store, notif_a_uri, ¬if_a, &fetcher_a)
|
|
.expect("seed source a");
|
|
|
|
let notif_b_uri = "https://example.net/b/notification.xml";
|
|
let snapshot_b_uri = "https://example.net/b/snapshot.xml";
|
|
let snapshot_b = snapshot_xml(sid_b, 1, &[(uri, b"b1")]);
|
|
let snapshot_b_hash = hex::encode(sha2::Sha256::digest(&snapshot_b));
|
|
let notif_b = notification_xml(sid_b, 1, snapshot_b_uri, &snapshot_b_hash);
|
|
let fetcher_b = MapFetcher {
|
|
map: HashMap::from([(snapshot_b_uri.to_string(), snapshot_b)]),
|
|
};
|
|
|
|
let err = sync_from_notification_snapshot(&store, notif_b_uri, ¬if_b, &fetcher_b)
|
|
.expect_err("cross-source overwrite must fail");
|
|
assert!(matches!(err, RrdpSyncError::Storage(_)));
|
|
assert!(err.to_string().contains("owner conflict"), "{err}");
|
|
}
|
|
|
|
struct PanicOnRrdpFetch;
|
|
|
|
impl Fetcher for PanicOnRrdpFetch {
|
|
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
|
panic!("cross-origin notification must be rejected before fetch: {uri}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cross_origin_snapshot_reference_is_rejected_before_fetch_or_state_write() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let notification_uri = "https://origin.example.test/notification.xml";
|
|
let notification = notification_xml(
|
|
"550e8400-e29b-41d4-a716-446655440000",
|
|
1,
|
|
"https://foreign.example.test/snapshot.xml",
|
|
&"00".repeat(32),
|
|
);
|
|
|
|
let err =
|
|
sync_from_notification_snapshot(&store, notification_uri, ¬ification, &PanicOnRrdpFetch)
|
|
.expect_err("cross-origin snapshot must be rejected");
|
|
assert!(matches!(
|
|
err,
|
|
RrdpSyncError::Rrdp(RrdpError::CrossOriginReference {
|
|
resource: RrdpResourceKind::Snapshot,
|
|
..
|
|
})
|
|
));
|
|
assert!(
|
|
load_rrdp_local_state(&store, notification_uri)
|
|
.expect("read state")
|
|
.is_none(),
|
|
"rejection must precede RRDP state writes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_origin_delta_reference_is_rejected_even_when_delta_would_not_be_used() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let notification_uri = "https://origin.example.test/notification.xml";
|
|
let notification = notification_xml_with_deltas(
|
|
"550e8400-e29b-41d4-a716-446655440000",
|
|
1,
|
|
"https://origin.example.test/snapshot.xml",
|
|
&"00".repeat(32),
|
|
&[(
|
|
"delta-1",
|
|
1,
|
|
"https://foreign.example.test/delta-1.xml",
|
|
&"11".repeat(32),
|
|
)],
|
|
);
|
|
|
|
let err = sync_from_notification(&store, notification_uri, ¬ification, &PanicOnRrdpFetch)
|
|
.expect_err("cross-origin delta must be rejected before state/fetch");
|
|
assert!(matches!(
|
|
err,
|
|
RrdpSyncError::Rrdp(RrdpError::CrossOriginReference {
|
|
resource: RrdpResourceKind::Delta,
|
|
..
|
|
})
|
|
));
|
|
assert!(
|
|
load_rrdp_local_state(&store, notification_uri)
|
|
.expect("read state")
|
|
.is_none(),
|
|
"all delta references are checked before state reads or writes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rrdp_origin_treats_https_default_port_as_equivalent() {
|
|
let notification = parse_notification(¬ification_xml(
|
|
"550e8400-e29b-41d4-a716-446655440000",
|
|
1,
|
|
"https://origin.example.test:443/snapshot.xml",
|
|
&"00".repeat(32),
|
|
))
|
|
.expect("notification");
|
|
validate_notification_references(
|
|
"https://origin.example.test/notification.xml",
|
|
¬ification,
|
|
)
|
|
.expect("explicit HTTPS default port must remain same-origin");
|
|
}
|
|
|
|
#[test]
|
|
fn sync_from_notification_snapshot_applies_snapshot_and_stores_state() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
|
|
|
|
let sid = "550e8400-e29b-41d4-a716-446655440000";
|
|
let serial = 9u64;
|
|
let notif_uri = "https://example.net/notification.xml";
|
|
let snapshot_uri = "https://example.net/snapshot.xml";
|
|
|
|
let snapshot = snapshot_xml(
|
|
sid,
|
|
serial,
|
|
&[
|
|
("rsync://example.net/repo/a.mft", b"mft-bytes"),
|
|
("rsync://example.net/repo/b.roa", b"roa-bytes"),
|
|
],
|
|
);
|
|
let snapshot_hash = hex::encode(sha2::Sha256::digest(&snapshot));
|
|
let notif = notification_xml(sid, serial, snapshot_uri, &snapshot_hash);
|
|
|
|
let fetcher = MapFetcher {
|
|
map: HashMap::from([(snapshot_uri.to_string(), snapshot.clone())]),
|
|
};
|
|
|
|
let published =
|
|
sync_from_notification_snapshot(&store, notif_uri, ¬if, &fetcher).expect("sync");
|
|
assert_eq!(published, 2);
|
|
|
|
assert_current_object(&store, "rsync://example.net/repo/a.mft", b"mft-bytes");
|
|
assert_current_object(&store, "rsync://example.net/repo/b.roa", b"roa-bytes");
|
|
|
|
let state = load_rrdp_local_state(&store, notif_uri)
|
|
.expect("get rrdp state")
|
|
.expect("state present");
|
|
assert_eq!(state.session_id, sid);
|
|
assert_eq!(state.serial, serial);
|
|
|
|
let source = store
|
|
.get_rrdp_source_record(notif_uri)
|
|
.expect("get rrdp source")
|
|
.expect("rrdp source exists");
|
|
assert_eq!(source.last_session_id.as_deref(), Some(sid));
|
|
assert_eq!(source.last_serial, Some(serial));
|
|
assert_eq!(
|
|
source.sync_state,
|
|
crate::repository::storage::RrdpSourceSyncState::SnapshotOnly
|
|
);
|
|
|
|
let view = store
|
|
.get_repository_view_entry("rsync://example.net/repo/a.mft")
|
|
.expect("get repository view")
|
|
.expect("repository view exists");
|
|
assert_eq!(
|
|
view.state,
|
|
crate::repository::storage::RepositoryViewState::Present
|
|
);
|
|
assert_eq!(view.repository_source.as_deref(), Some(notif_uri));
|
|
|
|
let current_bytes = store
|
|
.load_current_object_bytes_by_uri("rsync://example.net/repo/a.mft")
|
|
.expect("load current bytes")
|
|
.expect("current object bytes exist");
|
|
assert_eq!(current_bytes, b"mft-bytes".to_vec());
|
|
assert!(
|
|
store
|
|
.get_raw_by_hash_entry(hex::encode(sha2::Sha256::digest(b"mft-bytes")).as_str())
|
|
.expect("get raw_by_hash")
|
|
.is_none()
|
|
);
|
|
|
|
let member = store
|
|
.get_rrdp_source_member_record(notif_uri, "rsync://example.net/repo/a.mft")
|
|
.expect("get member")
|
|
.expect("member exists");
|
|
assert!(member.present);
|
|
let owner = store
|
|
.get_rrdp_uri_owner_record("rsync://example.net/repo/a.mft")
|
|
.expect("get owner")
|
|
.expect("owner exists");
|
|
assert_eq!(owner.notify_uri, notif_uri);
|
|
assert_eq!(
|
|
owner.owner_state,
|
|
crate::repository::storage::RrdpUriOwnerState::Active
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_from_notification_snapshot_deletes_objects_not_in_new_snapshot() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
|
|
|
|
let sid = "550e8400-e29b-41d4-a716-446655440000";
|
|
let notif_uri = "https://example.net/notification.xml";
|
|
|
|
// serial 1: publish a + b
|
|
let snapshot_uri_1 = "https://example.net/snapshot-1.xml";
|
|
let snapshot_1 = snapshot_xml(
|
|
sid,
|
|
1,
|
|
&[
|
|
("rsync://example.net/repo/a.mft", b"a1"),
|
|
("rsync://example.net/repo/b.roa", b"b1"),
|
|
],
|
|
);
|
|
let snapshot_hash_1 = hex::encode(sha2::Sha256::digest(&snapshot_1));
|
|
let notif_1 = notification_xml(sid, 1, snapshot_uri_1, &snapshot_hash_1);
|
|
|
|
let fetcher_1 = MapFetcher {
|
|
map: HashMap::from([(snapshot_uri_1.to_string(), snapshot_1)]),
|
|
};
|
|
sync_from_notification_snapshot(&store, notif_uri, ¬if_1, &fetcher_1).expect("sync 1");
|
|
|
|
// serial 2: publish b (new bytes) + c, and drop a
|
|
let snapshot_uri_2 = "https://example.net/snapshot-2.xml";
|
|
let snapshot_2 = snapshot_xml(
|
|
sid,
|
|
2,
|
|
&[
|
|
("rsync://example.net/repo/b.roa", b"b2"),
|
|
("rsync://example.net/repo/c.crl", b"c2"),
|
|
],
|
|
);
|
|
let snapshot_hash_2 = hex::encode(sha2::Sha256::digest(&snapshot_2));
|
|
let notif_2 = notification_xml(sid, 2, snapshot_uri_2, &snapshot_hash_2);
|
|
|
|
let fetcher_2 = MapFetcher {
|
|
map: HashMap::from([(snapshot_uri_2.to_string(), snapshot_2)]),
|
|
};
|
|
sync_from_notification_snapshot(&store, notif_uri, ¬if_2, &fetcher_2).expect("sync 2");
|
|
|
|
assert!(
|
|
store
|
|
.load_current_object_bytes_by_uri("rsync://example.net/repo/a.mft")
|
|
.expect("get current a")
|
|
.is_none(),
|
|
"a should be deleted by full-state snapshot apply"
|
|
);
|
|
|
|
assert_current_object(&store, "rsync://example.net/repo/b.roa", b"b2");
|
|
assert_current_object(&store, "rsync://example.net/repo/c.crl", b"c2");
|
|
}
|
|
|
|
#[test]
|
|
fn sync_from_notification_uses_deltas_when_available_for_local_state() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
|
|
|
|
let sid = "550e8400-e29b-41d4-a716-446655440000";
|
|
let notif_uri = "https://example.net/notification.xml";
|
|
|
|
// Seed state with snapshot serial=1 containing a+b.
|
|
let snapshot_uri_1 = "https://example.net/snapshot-1.xml";
|
|
let snapshot_1 = snapshot_xml(
|
|
sid,
|
|
1,
|
|
&[
|
|
("rsync://example.net/repo/a.mft", b"a1"),
|
|
("rsync://example.net/repo/b.roa", b"b1"),
|
|
],
|
|
);
|
|
let snapshot_hash_1 = hex::encode(sha2::Sha256::digest(&snapshot_1));
|
|
let notif_1 = notification_xml(sid, 1, snapshot_uri_1, &snapshot_hash_1);
|
|
let fetcher_1 = MapFetcher {
|
|
map: HashMap::from([(snapshot_uri_1.to_string(), snapshot_1)]),
|
|
};
|
|
sync_from_notification_snapshot(&store, notif_uri, ¬if_1, &fetcher_1).expect("seed");
|
|
|
|
// Notification serial=3 with deltas 2 and 3. Snapshot URI is intentionally not fetchable
|
|
// to assert we really use deltas.
|
|
let snapshot_uri_3 = "https://example.net/snapshot-3.xml";
|
|
let snapshot_hash_3 = "00".repeat(32);
|
|
|
|
let publish_c_b64 = base64::engine::general_purpose::STANDARD.encode(b"c2");
|
|
let delta_2 = delta_xml(
|
|
sid,
|
|
2,
|
|
&[&format!(
|
|
r#"<publish uri="rsync://example.net/repo/c.crl">{publish_c_b64}</publish>"#
|
|
)],
|
|
);
|
|
let delta_2_hash_hex = hex::encode(sha2::Sha256::digest(&delta_2));
|
|
|
|
let c_hash_hex = hex::encode(sha2::Sha256::digest(b"c2".as_slice()));
|
|
let delta_3 = delta_xml(
|
|
sid,
|
|
3,
|
|
&[&format!(
|
|
r#"<withdraw uri="rsync://example.net/repo/c.crl" hash="{c_hash_hex}"/>"#
|
|
)],
|
|
);
|
|
let delta_3_hash_hex = hex::encode(sha2::Sha256::digest(&delta_3));
|
|
|
|
let notif_3 = notification_xml_with_deltas(
|
|
sid,
|
|
3,
|
|
snapshot_uri_3,
|
|
&snapshot_hash_3,
|
|
&[
|
|
(
|
|
"d3",
|
|
3,
|
|
"https://example.net/delta-3.xml",
|
|
&delta_3_hash_hex,
|
|
),
|
|
(
|
|
"d2",
|
|
2,
|
|
"https://example.net/delta-2.xml",
|
|
&delta_2_hash_hex,
|
|
),
|
|
],
|
|
);
|
|
|
|
let fetcher = MapFetcher {
|
|
map: HashMap::from([
|
|
("https://example.net/delta-2.xml".to_string(), delta_2),
|
|
("https://example.net/delta-3.xml".to_string(), delta_3),
|
|
]),
|
|
};
|
|
|
|
let applied = sync_from_notification(&store, notif_uri, ¬if_3, &fetcher).expect("sync");
|
|
assert!(applied > 0);
|
|
|
|
// Delta 2 publishes c then delta 3 withdraws it => final state should not contain c.
|
|
assert!(
|
|
store
|
|
.load_current_object_bytes_by_uri("rsync://example.net/repo/c.crl")
|
|
.expect("get current")
|
|
.is_none()
|
|
);
|
|
|
|
let state = load_rrdp_local_state(&store, notif_uri)
|
|
.expect("get rrdp state")
|
|
.expect("state present");
|
|
assert_eq!(state.session_id, Uuid::parse_str(sid).unwrap().to_string());
|
|
assert_eq!(state.serial, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_from_notification_same_serial_hydrates_current_repo_index() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
|
|
|
|
let sid = "550e8400-e29b-41d4-a716-446655440000";
|
|
let notif_uri = "https://example.net/notification.xml";
|
|
let snapshot_uri = "https://example.net/snapshot.xml";
|
|
let uri_a = "rsync://example.net/repo/a.mft";
|
|
let uri_b = "rsync://example.net/repo/b.roa";
|
|
|
|
let snapshot = snapshot_xml(sid, 1, &[(uri_a, b"a1"), (uri_b, b"b1")]);
|
|
let snapshot_hash = hex::encode(sha2::Sha256::digest(&snapshot));
|
|
let notif = notification_xml(sid, 1, snapshot_uri, &snapshot_hash);
|
|
let fetcher_1 = MapFetcher {
|
|
map: HashMap::from([(snapshot_uri.to_string(), snapshot)]),
|
|
};
|
|
sync_from_notification_snapshot(&store, notif_uri, ¬if, &fetcher_1).expect("seed");
|
|
|
|
let index = CurrentRepoIndex::shared();
|
|
let no_fetcher = MapFetcher {
|
|
map: HashMap::new(),
|
|
};
|
|
let applied = sync_from_notification_with_timing_and_download_log(
|
|
&store,
|
|
notif_uri,
|
|
Some(&index),
|
|
¬if,
|
|
&no_fetcher,
|
|
None,
|
|
None,
|
|
)
|
|
.expect("same serial no-op");
|
|
assert_eq!(applied, 0);
|
|
|
|
let index = index.read().expect("read-lock index");
|
|
assert_eq!(index.active_uri_count(), 2);
|
|
assert!(index.get_by_uri(uri_a).is_some());
|
|
assert!(index.get_by_uri(uri_b).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn sync_from_notification_delta_hydrates_unchanged_current_repo_entries() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
|
|
|
|
let sid = "550e8400-e29b-41d4-a716-446655440000";
|
|
let notif_uri = "https://example.net/notification.xml";
|
|
let snapshot_uri_1 = "https://example.net/snapshot-1.xml";
|
|
let uri_a = "rsync://example.net/repo/a.mft";
|
|
let uri_b = "rsync://example.net/repo/b.roa";
|
|
let uri_c = "rsync://example.net/repo/c.crl";
|
|
|
|
let snapshot_1 = snapshot_xml(sid, 1, &[(uri_a, b"a1"), (uri_b, b"b1")]);
|
|
let snapshot_hash_1 = hex::encode(sha2::Sha256::digest(&snapshot_1));
|
|
let notif_1 = notification_xml(sid, 1, snapshot_uri_1, &snapshot_hash_1);
|
|
let fetcher_1 = MapFetcher {
|
|
map: HashMap::from([(snapshot_uri_1.to_string(), snapshot_1)]),
|
|
};
|
|
sync_from_notification_snapshot(&store, notif_uri, ¬if_1, &fetcher_1).expect("seed");
|
|
|
|
let publish_c_b64 = base64::engine::general_purpose::STANDARD.encode(b"c2");
|
|
let delta_2 = delta_xml(
|
|
sid,
|
|
2,
|
|
&[&format!(
|
|
r#"<publish uri="{uri_c}">{publish_c_b64}</publish>"#
|
|
)],
|
|
);
|
|
let delta_2_hash_hex = hex::encode(sha2::Sha256::digest(&delta_2));
|
|
let notif_2 = notification_xml_with_deltas(
|
|
sid,
|
|
2,
|
|
"https://example.net/snapshot-2.xml",
|
|
&"00".repeat(32),
|
|
&[(
|
|
"d2",
|
|
2,
|
|
"https://example.net/delta-2.xml",
|
|
&delta_2_hash_hex,
|
|
)],
|
|
);
|
|
let fetcher_2 = MapFetcher {
|
|
map: HashMap::from([("https://example.net/delta-2.xml".to_string(), delta_2)]),
|
|
};
|
|
|
|
let index = CurrentRepoIndex::shared();
|
|
let applied = sync_from_notification_with_timing_and_download_log(
|
|
&store,
|
|
notif_uri,
|
|
Some(&index),
|
|
¬if_2,
|
|
&fetcher_2,
|
|
None,
|
|
None,
|
|
)
|
|
.expect("delta sync");
|
|
assert_eq!(applied, 1);
|
|
|
|
let index = index.read().expect("read-lock index");
|
|
assert_eq!(index.active_uri_count(), 3);
|
|
assert!(
|
|
index.get_by_uri(uri_a).is_some(),
|
|
"unchanged object from the previous serial must be visible"
|
|
);
|
|
assert!(
|
|
index.get_by_uri(uri_b).is_some(),
|
|
"unchanged object from the previous serial must be visible"
|
|
);
|
|
assert!(index.get_by_uri(uri_c).is_some(), "delta publish visible");
|
|
}
|