aboutsummaryrefslogblamecommitdiffstats
path: root/filamento/src/caps.rs
blob: 819e669c102244e7b721c5d5601fff48f3d5a74f (plain) (tree)
1
2
3
4
5
6
7
8
9


                                               
               



                           
                        
                                 
             
  
                   

            




                                                                                                

  














                                                                                                                                     
                                                          

                                                                 
                                                                 
                                                          
          
                                                         






                                                             































































































                                                                                       
                                                   


































































                                                            









                                                        














                                                         
                     











                                                                                      
                                                                          












































                                                                                      















                                                                         




















                                                                            











































































































                                                                                           







                                                                              

                                             



































                                                                                 

     
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);
    }
}