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::Category},
error::{CapsDecodeError, CapsEncodeError, HashNodeConversionError},
};
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 caps2(query: info::Query) -> xep_0390::C {
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');
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 info(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 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(),
})
}
static CLIENT_INFO: Info = Info {
node: None,
features: vec![],
identities: vec![],
};
#[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 caps = caps("https://macaw.chat".to_string(), info).unwrap();
let stdout = tokio::io::stdout();
let mut writer = Writer::new(stdout);
writer.write(&caps).await;
}
}