commit d246fa5b4d3de7647bd4f64ecc561f215106f593
parent 519c9f2e74760a2bac06bb28d57ba43dcff78f8f
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Jul 2026 19:04:15 +0000
transport: reject noncanonical publish target kinds
- classify explicit publish targets through the shared canonical kind parser
- reject mixed-case Reticulum targets before job creation
- reject removed proxy kind requests before dispatch
- cover invalid target-kind requests in daemon publish tests
Diffstat:
2 files changed, 82 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1918,6 +1918,7 @@ dependencies = [
name = "radroots_transport_publish_protocol"
version = "0.1.0-alpha.2"
dependencies = [
+ "radroots_transport",
"serde",
]
diff --git a/src/core/transport_publish.rs b/src/core/transport_publish.rs
@@ -21,7 +21,7 @@ use radroots_relay_transport::{
RadrootsRelayPublishAdapter, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest,
RadrootsRelayTargetSet, RadrootsRelayTransportError, RadrootsRelayUrl, RadrootsRelayUrlPolicy,
};
-use radroots_transport::RadrootsTransportSatisfactionPolicy;
+use radroots_transport::{RadrootsTransportKind, RadrootsTransportSatisfactionPolicy};
use radroots_transport_publish_protocol::{
NostrPublishTargetSourcePolicy, SignedNostrEventWire, TransportPublishDeliveryPolicy,
TransportPublishEventRequest, TransportPublishEventResponse, TransportPublishJobStatus,
@@ -229,13 +229,19 @@ impl TransportPublish {
) -> Result<PublishRelayResolution, TransportPublishError> {
let mut resolved = Vec::new();
let mut outcomes = Vec::new();
- for target in targets {
- match target.transport_kind.as_str() {
- TRANSPORT_KIND_NOSTR => {
+ for (index, target) in targets.iter().enumerate() {
+ match RadrootsTransportKind::parse_canonical(target.transport_kind.as_str()).map_err(
+ |error| {
+ TransportPublishError::InvalidSignedEvent(format!(
+ "transport target {index} kind is invalid: {error}"
+ ))
+ },
+ )? {
+ RadrootsTransportKind::Nostr => {
self.resolve_request_target(&mut resolved, &mut outcomes, target)
.await;
}
- TRANSPORT_KIND_RETICULUM => {
+ RadrootsTransportKind::Reticulum => {
outcomes.push(reticulum_preview_outcome(target));
}
_ => outcomes.push(unsupported_transport_outcome(target)),
@@ -2954,6 +2960,76 @@ mod tests {
}
#[tokio::test]
+ async fn publish_event_rejects_noncanonical_reticulum_kind_before_recording_job() {
+ let identity = RadrootsIdentity::generate();
+ let (proxy, adapter) = transport_publish(TransportPublishConfig::default());
+ let principal =
+ explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own);
+ let mut request = reticulum_publish_request(
+ signed_event(&identity, "{}"),
+ TransportPublishPreviewBehavior::RejectDeliveryAttempts,
+ );
+ request.target_policy =
+ TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget {
+ transport_kind: "Reticulum".to_owned(),
+ endpoint_uri: "reticulum:preview-unavailable".to_owned(),
+ preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts),
+ }]);
+
+ let err = proxy
+ .publish_event(&principal, request)
+ .await
+ .expect_err("noncanonical Reticulum kind");
+
+ assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_)));
+ assert!(adapter.captured_raw_events().is_empty());
+ assert!(
+ proxy
+ .store
+ .list_jobs_for_principal(&principal, 10)
+ .expect("jobs")
+ .is_empty()
+ );
+ }
+
+ #[tokio::test]
+ async fn publish_event_rejects_removed_proxy_kind_before_recording_job() {
+ let identity = RadrootsIdentity::generate();
+ let (proxy, adapter) = transport_publish(TransportPublishConfig::default());
+ let principal =
+ explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own);
+ let mut request = reticulum_publish_request(
+ signed_event(&identity, "{}"),
+ TransportPublishPreviewBehavior::RejectDeliveryAttempts,
+ );
+ request.target_policy =
+ TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget {
+ transport_kind: removed_proxy_kind_string(),
+ endpoint_uri: "radrootsd-proxy:publish".to_owned(),
+ preview_behavior: None,
+ }]);
+
+ let err = proxy
+ .publish_event(&principal, request)
+ .await
+ .expect_err("removed proxy kind");
+
+ assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_)));
+ assert!(adapter.captured_raw_events().is_empty());
+ assert!(
+ proxy
+ .store
+ .list_jobs_for_principal(&principal, 10)
+ .expect("jobs")
+ .is_empty()
+ );
+ }
+
+ fn removed_proxy_kind_string() -> String {
+ ["radrootsd", "_proxy"].concat()
+ }
+
+ #[tokio::test]
async fn publish_event_records_unsafe_request_relay_rejection() {
let identity = RadrootsIdentity::generate();
let (proxy, adapter) = transport_publish(TransportPublishConfig::default());