use std::str::FromStr; use base64::{Engine, prelude::BASE64_STANDARD}; use sha1::Sha1; use sha2::{Digest, Sha256}; use sha3::Sha3_256; use stanza::{ xep_0030::info, xep_0115::{self, C}, xep_0300::{self, Algo, Hash}, xep_0390, }; use tracing::trace; use crate::{ disco::{ Identity, Info, identity::{self, Category}, }, error::{CapsDecodeError, CapsEncodeError, CapsNodeConversionError, HashNodeConversionError}, }; pub const CLIENT_URI: &str = "https://bunny.garden/filamento"; // pub fn c() -> C { caps(CLIENT_URI.to_string(), client_info().into()).unwrap() } pub fn caps_node() -> String { caps_to_node(c()) } pub fn client_info() -> Info { Info { node: None, features: vec![ "http://jabber.org/protocol/caps".to_string(), "http://jabber.org/protocol/disco#info".to_string(), "http://jabber.org/protocol/disco#items".to_string(), "http://jabber.org/protocol/nick+notify".to_string(), "urn:xmpp:avatar:metadata+notify".to_string(), ], // "http://jabber.org/protocol/nick".to_string(), identities: vec![Identity { name: Some("filamento 0.1.0".to_string()), category: Category::Client(identity::Client::PC), }], } } pub fn caps(node: String, query: info::Query) -> Result { let mut string = String::new(); // identities string let mut identities = Vec::new(); for identity in query.identities { let mut string = String::new(); string.push_str(&identity.category); string.push('/'); string.push_str(&identity.r#type); string.push('/'); string.push_str(&identity.lang.unwrap_or_default()); string.push('/'); string.push_str(&identity.name.unwrap_or_default()); string.push('<'); identities.push(string); } identities.sort(); let identities_string = identities.concat(); string.push_str(&identities_string); // features string let mut features = Vec::new(); for feature in query.features { let mut string = String::new(); string.push_str(&feature.var); string.push('<'); features.push(string); } features.sort(); let features_string = features.concat(); string.push_str(&features_string); // extensions string let mut extensions = Vec::new(); for extension in query.extensions { let mut string = String::new(); let form_type = extension .fields .iter() .find(|field| field.var.as_deref() == Some("FORM_TYPE")) .ok_or(CapsEncodeError::InvalidDataForm)? .values .clone() .into_iter() .map(|value| value.0) .collect::>() .concat(); string.push_str(&form_type); string.push('<'); let mut fields = Vec::new(); for field in extension.fields { if field.var.as_deref() == Some("FORM_TYPE") { continue; } let mut string = String::new(); string.push_str(&field.var.unwrap_or_default()); string.push('<'); let mut values = Vec::new(); for value in field.values { let mut string = String::new(); string.push_str(&value.0); string.push('<'); values.push(string); } values.sort(); let values_string = values.concat(); string.push_str(&values_string); fields.push(string); } fields.sort(); let fields_string = fields.concat(); string.push_str(&fields_string); extensions.push(string); } extensions.sort(); let extensions_string = extensions.concat(); string.push_str(&extensions_string); trace!("generated caps string: `{}`", string); let mut sha1 = Sha1::new(); sha1.update(&string); let result = sha1.finalize(); let sha1_result = BASE64_STANDARD.encode(result); Ok(C { ext: None, hash: "sha-1".to_string(), node, ver: sha1_result, }) } pub fn encode_caps2(query: info::Query) -> String { let mut string = String::new(); // features string let mut features = Vec::new(); for feature in query.features { let mut string = String::new(); string.push_str(&feature.var); string.push('\x1f'); features.push(string); } features.sort(); let features_string = features.concat(); string.push_str(&features_string); string.push('\x1c'); // identities string let mut identities = Vec::new(); for identity in query.identities { let mut string = String::new(); string.push_str(&identity.category); string.push('\x1f'); string.push_str(&identity.r#type); string.push('\x1f'); string.push_str(&identity.lang.unwrap_or_default()); string.push('\x1f'); string.push_str(&identity.name.unwrap_or_default()); string.push('\x1f'); string.push('\x1e'); identities.push(string); } identities.sort(); let identities_string = identities.concat(); string.push_str(&identities_string); string.push('\x1c'); // extensions string let mut extensions = Vec::new(); for extension in query.extensions { let mut string = String::new(); let mut fields = Vec::new(); for field in extension.fields { let mut string = String::new(); string.push_str(&field.var.unwrap_or_default()); string.push('\x1f'); let mut values = Vec::new(); for value in field.values { let mut string = String::new(); string.push_str(&value.0); string.push('\x1f'); values.push(string); } values.sort(); let values_string = values.concat(); string.push_str(&values_string); string.push('\x1e'); fields.push(string); } fields.sort(); let fields_string = fields.concat(); string.push_str(&fields_string); string.push('\x1d'); extensions.push(string); } extensions.sort(); let extensions_string = extensions.concat(); string.push_str(&extensions_string); string.push('\x1c'); string } pub fn encode_info_base64(info: info::Query) -> String { let string = encode_caps2(info); BASE64_STANDARD.encode(string) } pub fn caps2(query: info::Query) -> xep_0390::C { let string = encode_caps2(query); let mut sha256 = Sha256::new(); sha256.update(&string); let result = sha256.finalize(); let sha256_result = BASE64_STANDARD.encode(result); let mut sha3_256 = Sha3_256::new(); sha3_256.update(string); let result = sha3_256.finalize(); let sha3_256_result = BASE64_STANDARD.encode(result); xep_0390::C(vec![ Hash { algo: Algo::SHA256, hash: sha256_result, }, Hash { algo: Algo::SHA3256, hash: sha3_256_result, }, ]) } /// takes a base64 encoded cached caps string and converts it into a disco info result pub fn decode_info_base64(info: String) -> Result { let info = String::from_utf8(BASE64_STANDARD.decode(info)?)?; let mut strings = info.split_terminator('\x1c'); let features_string = strings.next().ok_or(CapsDecodeError::MissingFeatures)?; let mut features = Vec::new(); for feature in features_string.split_terminator('\x1f') { features.push(feature.to_owned()); } let identities_string = strings.next().ok_or(CapsDecodeError::MissingIdentities)?; let mut identities = Vec::new(); for identity in identities_string.split_terminator('\x1e') { let mut identity_string = identity.split_terminator('\x1f'); let category = identity_string .next() .ok_or(CapsDecodeError::MissingIdentityCategory)?; let r#type = identity_string .next() .ok_or(CapsDecodeError::MissingIdentityType)?; let _ = identity_string .next() .ok_or(CapsDecodeError::MissingIdentityLang)?; let name = identity_string .next() .ok_or(CapsDecodeError::MissingIdentityName)?; let name = if name.is_empty() { None } else { Some(name.to_string()) }; let category = Category::from_category_and_type(category, r#type); identities.push(Identity { name, category }) } // TODO: service discovery extensions Ok(Info { node: None, features, identities, }) } pub fn caps_to_node(caps: C) -> String { caps.node + "#" + caps.ver.as_str() } pub fn node_to_caps(node: String) -> Result { let (node, ver) = node .rsplit_once('#') .ok_or(CapsNodeConversionError::MissingHashtag)?; Ok(C { ext: None, hash: "sha-1".to_string(), node: node.to_string(), ver: ver.to_string(), }) } pub fn hash_to_node(hash: xep_0300::Hash) -> String { let mut string = String::from("urn:xmpp:caps#"); string.push_str(&hash.algo.to_string()); string.push('.'); string.push_str(&hash.hash); string } pub fn node_to_hash(node: String) -> Result { let string = node .strip_prefix("urn:xmpp:caps#") .ok_or(HashNodeConversionError::NoPrefix)?; let (algo, hash) = string .rsplit_once('.') .ok_or(HashNodeConversionError::MissingPeriod)?; Ok(Hash { algo: Algo::from_str(algo).unwrap(), hash: hash.to_string(), }) } #[cfg(test)] mod tests { use peanuts::{Writer, element::IntoElement}; use stanza::{ xep_0004::{Field, FieldType, Value, X, XType}, xep_0030::info::{Feature, Identity}, }; use super::*; #[tokio::test] async fn test_caps() { tracing_subscriber::fmt().init(); let info = info::Query { node: Some("http://psi-im.org".to_string()), features: vec![ Feature { var: "http://jabber.org/protocol/caps".to_string(), }, Feature { var: "http://jabber.org/protocol/disco#items".to_string(), }, Feature { var: "http://jabber.org/protocol/disco#info".to_string(), }, Feature { var: "http://jabber.org/protocol/muc".to_string(), }, ], identities: vec![ Identity { category: "client".to_string(), name: Some("Ψ 0.11".to_string()), r#type: "pc".to_string(), lang: Some("el".to_string()), }, Identity { category: "client".to_string(), name: Some("Psi 0.11".to_string()), r#type: "pc".to_string(), lang: Some("en".to_string()), }, ], extensions: vec![X { r#type: XType::Result, instructions: Vec::new(), title: None, fields: vec![ Field { label: None, r#type: None, var: Some("ip_version".to_string()), desc: None, required: false, values: vec![Value("ipv4".to_string()), Value("ipv6".to_string())], options: Vec::new(), }, Field { label: None, r#type: None, var: Some("software".to_string()), desc: None, required: false, values: vec![Value("Psi".to_string())], options: Vec::new(), }, Field { label: None, r#type: Some(FieldType::Hidden), var: Some("FORM_TYPE".to_string()), desc: None, required: false, values: vec![Value("urn:xmpp:dataforms:softwareinfo".to_string())], options: Vec::new(), }, Field { label: None, r#type: None, var: Some("os".to_string()), desc: None, required: false, values: vec![Value("Mac".to_string())], options: Vec::new(), }, Field { label: None, r#type: None, var: Some("os_version".to_string()), desc: None, required: false, values: vec![Value("10.5.1".to_string())], options: Vec::new(), }, Field { label: None, r#type: None, var: Some("software_version".to_string()), desc: None, required: false, values: vec![Value("0.11".to_string())], options: Vec::new(), }, ], reported: None, items: Vec::new(), }], }; let test_caps = caps("https://macaw.chat".to_string(), info).unwrap(); let stdout = tokio::io::stdout(); let mut writer = Writer::new(stdout); writer.write(&test_caps).await.unwrap(); } #[tokio::test] pub async fn test_gen_client_caps() { let stdout = tokio::io::stdout(); let mut writer = Writer::new(stdout); // let info = info::Query { // node: Some("https://bunny.garden/filamento".to_string()), // features: vec![ // Feature { // var: "http://jabber.org/protocol/disco#items".to_string(), // }, // Feature { // var: "http://jabber.org/protocol/disco#info".to_string(), // }, // Feature { // var: "http://jabber.org/protocol/caps".to_string(), // }, // Feature { // var: "http://jabber.org/protocol/nick".to_string(), // }, // Feature { // var: "http://jabber.org/protocol/nick+notify".to_string(), // }, // ], // identities: vec![Identity { // category: "client".to_string(), // name: Some("filamento 0.1.0".to_string()), // r#type: "pc".to_string(), // lang: None, // }], // extensions: vec![], // }; let info: info::Query = client_info().into(); let client_caps = caps(CLIENT_URI.to_string(), info.clone()).unwrap(); writer.write(&client_caps).await.unwrap(); let node = caps_to_node(client_caps); println!("client caps node: `{}`", node); let client_caps_base64 = encode_info_base64(info); println!("client caps: `{}`", client_caps_base64); let client_caps = decode_info_base64(client_caps_base64).unwrap(); println!("client caps: `{:?}`", client_caps); } }