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";
// <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" ver="mSavc/SLnHm8zazs5RlcbD/iXoc=" node="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<xep_0115::C, CapsEncodeError> {
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::<Vec<String>>()
.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<Info, CapsDecodeError> {
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<C, CapsNodeConversionError> {
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<Hash, HashNodeConversionError> {
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);
}
}