//! Runtime publication of application and protocol versions. //! //! Capability manifests are informational. They let applications explain //! interoperability problems and suggest an update, but do not authorize a //! peer or trigger any update action. use std::collections::BTreeMap; use federation_net::ByteStream; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncRead, AsyncReadExt}; use crate::error::{MusicDhtError, Result}; /// Auxiliary ALPN used to query a peer's version manifest. pub const CAPABILITIES_ALPN: &[u8] = b"furumi/capabilities/1"; /// Capability protocol and manifest envelope version. pub const CAPABILITIES_PROTOCOL_VERSION: u16 = 1; /// Maximum accepted capability JSON line. pub const MAX_CAPABILITIES_LINE: usize = 64 * 1024; /// Maximum protocol entries accepted from one peer. pub const MAX_PROTOCOL_ENTRIES: usize = 64; /// Stable protocol identifier for federation-net. pub const FEDERATION_NET_ID: &str = "federation_net"; /// Stable protocol identifier for endpoint tickets. pub const TICKET_ID: &str = "ticket"; /// Stable protocol identifier for Mainline-DHT rendezvous records. pub const RENDEZVOUS_ID: &str = "rendezvous"; /// Stable protocol identifier for music-dht. pub const MUSIC_DHT_ID: &str = "music_dht"; /// Stable protocol identifier for rich catalog streams. pub const CATALOG_ID: &str = "catalog"; /// Stable protocol identifier for personal-device synchronization. pub const DEVICE_SYNC_ID: &str = "device_sync"; /// Stable protocol identifier for Jam playback control. pub const JAM_ID: &str = "jam"; /// Application and protocol versions published by one peer. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CapabilityManifest { /// Manifest schema version. pub manifest_version: u16, /// Application family, for example `furumi`. pub application: String, /// User-visible application release. pub application_version: String, /// Supported/current protocol version by stable identifier. pub protocols: BTreeMap, } impl CapabilityManifest { /// Creates a manifest containing every protocol owned by Frid. pub fn frid(application: impl Into, application_version: impl Into) -> Self { let mut protocols = BTreeMap::new(); protocols.insert( FEDERATION_NET_ID.to_string(), federation_net::PROTOCOL_VERSION, ); protocols.insert(TICKET_ID.to_string(), federation_net::TICKET_VERSION); protocols.insert( RENDEZVOUS_ID.to_string(), federation_net::RENDEZVOUS_RECORD_VERSION, ); protocols.insert(MUSIC_DHT_ID.to_string(), crate::DHT_PROTOCOL_VERSION); protocols.insert( CATALOG_ID.to_string(), crate::catalog::CATALOG_PROTOCOL_VERSION, ); protocols.insert( DEVICE_SYNC_ID.to_string(), crate::device_sync::DEVICE_SYNC_PROTOCOL_VERSION, ); protocols.insert(JAM_ID.to_string(), crate::jam::JAM_PROTOCOL_VERSION); Self { manifest_version: CAPABILITIES_PROTOCOL_VERSION, application: application.into(), application_version: application_version.into(), protocols, } } /// Adds an application-owned protocol to the manifest. pub fn with_protocol(mut self, id: impl Into, version: u16) -> Self { self.protocols.insert(id.into(), version); self } /// Removes a shared protocol that this particular application does not /// expose, while retaining the canonical versions for the others. pub fn without_protocol(mut self, id: &str) -> Self { self.protocols.remove(id); self } /// Validates bounds and identifiers received from a peer. pub fn validate(&self) -> Result<()> { if self.manifest_version != CAPABILITIES_PROTOCOL_VERSION { return Err(protocol_error(format!( "unsupported capability manifest version {}", self.manifest_version ))); } if self.application.trim().is_empty() || self.application.len() > 64 || self.application_version.len() > 64 || self.protocols.len() > MAX_PROTOCOL_ENTRIES { return Err(protocol_error("invalid capability manifest bounds")); } for (id, version) in &self.protocols { if id.is_empty() || id.len() > 64 || !id .bytes() .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') || *version == 0 { return Err(protocol_error("invalid capability protocol entry")); } } Ok(()) } } /// One request/response message on [`CAPABILITIES_ALPN`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum CapabilityMessage { /// Requests the peer's current manifest. Get { /// Request protocol version. version: u16, }, /// Returns a manifest. Manifest { /// Published version manifest. manifest: CapabilityManifest, }, /// Refuses a malformed or unsupported request. Error { /// Human-readable diagnostic. message: String, }, } /// Writes one bounded JSON-lines capability message. pub async fn write_message(stream: &mut ByteStream, message: &CapabilityMessage) -> Result<()> { let mut bytes = serde_json::to_vec(message).map_err(protocol_error)?; if bytes.len() > MAX_CAPABILITIES_LINE { return Err(protocol_error("capability message is too large")); } bytes.push(b'\n'); stream.send.write_all(&bytes).await.map_err(network_error)?; Ok(()) } /// Reads one bounded JSON-lines capability message. pub async fn read_message(stream: &mut ByteStream) -> Result { read_message_from(&mut stream.recv).await } /// Reads one bounded JSON-lines capability message from an async reader. pub async fn read_message_from(reader: &mut R) -> Result { let mut bytes = Vec::new(); let mut byte = [0_u8; 1]; loop { let read = reader.read(&mut byte).await.map_err(network_error)?; if read == 0 || byte[0] == b'\n' { break; } bytes.push(byte[0]); if bytes.len() > MAX_CAPABILITIES_LINE { return Err(protocol_error("capability message is too large")); } } if bytes.is_empty() { return Err(protocol_error("capability message is empty")); } let message: CapabilityMessage = serde_json::from_slice(&bytes).map_err(protocol_error)?; if let CapabilityMessage::Manifest { manifest } = &message { manifest.validate()?; } Ok(message) } fn protocol_error(error: impl std::fmt::Display) -> MusicDhtError { MusicDhtError::Protocol(error.to_string()) } fn network_error(error: impl std::fmt::Display) -> MusicDhtError { MusicDhtError::Network(error.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn frid_manifest_contains_every_shared_protocol() { let manifest = CapabilityManifest::frid("furumi", "1.2.3"); for id in [ FEDERATION_NET_ID, TICKET_ID, RENDEZVOUS_ID, MUSIC_DHT_ID, CATALOG_ID, DEVICE_SYNC_ID, JAM_ID, ] { assert!(manifest.protocols.contains_key(id), "missing {id}"); } manifest.validate().unwrap(); } #[test] fn application_protocol_is_additive() { let manifest = CapabilityManifest::frid("furumi", "1.2.3").with_protocol("audio", 1); assert_eq!(manifest.protocols.get("audio"), Some(&1)); } #[test] fn invalid_protocol_identifier_is_rejected() { let manifest = CapabilityManifest::frid("furumi", "1.2.3").with_protocol("Audio Stream", 1); assert!(manifest.validate().is_err()); } }