caps.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use try_from::TryFrom;
  8
  9use disco::{Feature, Identity, DiscoInfoResult, DiscoInfoQuery};
 10use data_forms::DataForm;
 11use hashes::{Hash, Algo};
 12
 13use minidom::Element;
 14use error::Error;
 15use ns;
 16use base64;
 17
 18use sha1::Sha1;
 19use sha2::{Sha256, Sha512};
 20use sha3::{Sha3_256, Sha3_512};
 21use blake2::Blake2b;
 22use digest::{Digest, VariableOutput};
 23
 24#[derive(Debug, Clone)]
 25pub struct Caps {
 26    pub ext: Option<String>,
 27    pub node: String,
 28    pub hash: Hash,
 29}
 30
 31impl TryFrom<Element> for Caps {
 32    type Err = Error;
 33
 34    fn try_from(elem: Element) -> Result<Caps, Error> {
 35        check_self!(elem, "c", ns::CAPS, "caps");
 36        check_no_children!(elem, "caps");
 37        check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]);
 38        let ver: String = get_attr!(elem, "ver", required);
 39        let hash = Hash {
 40            algo: get_attr!(elem, "hash", required),
 41            hash: base64::decode(&ver)?,
 42        };
 43        Ok(Caps {
 44            ext: get_attr!(elem, "ext", optional),
 45            node: get_attr!(elem, "node", required),
 46            hash: hash,
 47        })
 48    }
 49}
 50
 51impl From<Caps> for Element {
 52    fn from(caps: Caps) -> Element {
 53        Element::builder("c")
 54                .ns(ns::CAPS)
 55                .attr("ext", caps.ext)
 56                .attr("hash", caps.hash.algo)
 57                .attr("node", caps.node)
 58                .attr("ver", base64::encode(&caps.hash.hash))
 59                .build()
 60    }
 61}
 62
 63fn compute_item(field: &str) -> Vec<u8> {
 64    let mut bytes = field.as_bytes().to_vec();
 65    bytes.push(b'<');
 66    bytes
 67}
 68
 69fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], encode: F) -> Vec<u8> {
 70    let mut string: Vec<u8> = vec!();
 71    let mut accumulator: Vec<Vec<u8>> = vec!();
 72    for thing in things {
 73        let bytes = encode(thing);
 74        accumulator.push(bytes);
 75    }
 76    // This works using the expected i;octet collation.
 77    accumulator.sort();
 78    for mut bytes in accumulator {
 79        string.append(&mut bytes);
 80    }
 81    string
 82}
 83
 84fn compute_features(features: &[Feature]) -> Vec<u8> {
 85    compute_items(features, |feature| compute_item(&feature.var))
 86}
 87
 88fn compute_identities(identities: &[Identity]) -> Vec<u8> {
 89    compute_items(identities, |identity| {
 90        let lang = identity.lang.clone().unwrap_or_default();
 91        let name = identity.name.clone().unwrap_or_default();
 92        let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name);
 93        let bytes = string.as_bytes();
 94        let mut vec = Vec::with_capacity(bytes.len());
 95        vec.extend_from_slice(bytes);
 96        vec.push(b'<');
 97        vec
 98    })
 99}
100
101fn compute_extensions(extensions: &[DataForm]) -> Vec<u8> {
102    compute_items(extensions, |extension| {
103        let mut bytes = vec!();
104        // TODO: maybe handle the error case?
105        if let Some(ref form_type) = extension.form_type {
106            bytes.extend_from_slice(form_type.as_bytes());
107        }
108        bytes.push(b'<');
109        for field in extension.fields.clone() {
110            if field.var == "FORM_TYPE" {
111                continue;
112            }
113            bytes.append(&mut compute_item(&field.var));
114            bytes.append(&mut compute_items(&field.values,
115                                            |value| compute_item(value)));
116        }
117        bytes
118    })
119}
120
121pub fn compute_disco(disco: &DiscoInfoResult) -> Vec<u8> {
122    let identities_string = compute_identities(&disco.identities);
123    let features_string = compute_features(&disco.features);
124    let extensions_string = compute_extensions(&disco.extensions);
125
126    let mut final_string = vec!();
127    final_string.extend(identities_string);
128    final_string.extend(features_string);
129    final_string.extend(extensions_string);
130    final_string
131}
132
133fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
134    let mut vec = Vec::with_capacity(hash.len());
135    vec.extend_from_slice(hash);
136    vec
137}
138
139pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
140    Ok(Hash {
141        hash: match algo {
142            Algo::Sha_1 => {
143                let hash = Sha1::digest(data);
144                get_hash_vec(hash.as_slice())
145            },
146            Algo::Sha_256 => {
147                let hash = Sha256::digest(data);
148                get_hash_vec(hash.as_slice())
149            },
150            Algo::Sha_512 => {
151                let hash = Sha512::digest(data);
152                get_hash_vec(hash.as_slice())
153            },
154            Algo::Sha3_256 => {
155                let hash = Sha3_256::digest(data);
156                get_hash_vec(hash.as_slice())
157            },
158            Algo::Sha3_512 => {
159                let hash = Sha3_512::digest(data);
160                get_hash_vec(hash.as_slice())
161            },
162            Algo::Blake2b_256 => {
163                let mut hasher = Blake2b::default();
164                hasher.input(data);
165                let mut buf: [u8; 32] = [0; 32];
166                let hash = hasher.variable_result(&mut buf).unwrap();
167                get_hash_vec(hash)
168            },
169            Algo::Blake2b_512 => {
170                let mut hasher = Blake2b::default();
171                hasher.input(data);
172                let mut buf: [u8; 64] = [0; 64];
173                let hash = hasher.variable_result(&mut buf).unwrap();
174                get_hash_vec(hash)
175            },
176            Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
177        },
178        algo: algo,
179    })
180}
181
182pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
183    DiscoInfoQuery {
184        node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use caps;
192    use base64;
193
194    #[test]
195    fn test_parse() {
196        let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
197        let caps = Caps::try_from(elem).unwrap();
198        assert_eq!(caps.node, String::from("coucou"));
199        assert_eq!(caps.hash.algo, Algo::Sha_256);
200        assert_eq!(caps.hash.hash, base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap());
201    }
202
203    #[test]
204    fn test_invalid_child() {
205        let elem: Element = "<c xmlns='http://jabber.org/protocol/caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash></c>".parse().unwrap();
206        let error = Caps::try_from(elem).unwrap_err();
207        let message = match error {
208            Error::ParseError(string) => string,
209            _ => panic!(),
210        };
211        assert_eq!(message, "Unknown child in caps element.");
212    }
213
214    #[test]
215    fn test_simple() {
216        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
217        let disco = DiscoInfoResult::try_from(elem).unwrap();
218        let caps = caps::compute_disco(&disco);
219        assert_eq!(caps.len(), 50);
220    }
221
222    #[test]
223    fn test_xep_5_2() {
224        let elem: Element = r#"
225<query xmlns='http://jabber.org/protocol/disco#info'
226       node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
227  <identity category='client' name='Exodus 0.9.1' type='pc'/>
228  <feature var='http://jabber.org/protocol/caps'/>
229  <feature var='http://jabber.org/protocol/disco#info'/>
230  <feature var='http://jabber.org/protocol/disco#items'/>
231  <feature var='http://jabber.org/protocol/muc'/>
232</query>
233"#.parse().unwrap();
234        
235        let data = b"client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
236        let mut expected = Vec::with_capacity(data.len());
237        expected.extend_from_slice(data);
238        let disco = DiscoInfoResult::try_from(elem).unwrap();
239        let caps = caps::compute_disco(&disco);
240        assert_eq!(caps, expected);
241
242        let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
243        assert_eq!(sha_1.hash, base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap());
244    }
245
246    #[test]
247    fn test_xep_5_3() {
248        let elem: Element = r#"
249<query xmlns='http://jabber.org/protocol/disco#info'
250       node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
251  <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
252  <identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
253  <feature var='http://jabber.org/protocol/caps'/>
254  <feature var='http://jabber.org/protocol/disco#info'/>
255  <feature var='http://jabber.org/protocol/disco#items'/>
256  <feature var='http://jabber.org/protocol/muc'/>
257  <x xmlns='jabber:x:data' type='result'>
258    <field var='FORM_TYPE' type='hidden'>
259      <value>urn:xmpp:dataforms:softwareinfo</value>
260    </field>
261    <field var='ip_version'>
262      <value>ipv4</value>
263      <value>ipv6</value>
264    </field>
265    <field var='os'>
266      <value>Mac</value>
267    </field>
268    <field var='os_version'>
269      <value>10.5.1</value>
270    </field>
271    <field var='software'>
272      <value>Psi</value>
273    </field>
274    <field var='software_version'>
275      <value>0.11</value>
276    </field>
277  </x>
278</query>
279"#.parse().unwrap();
280        let data = b"client/pc/el/\xce\xa8 0.11<client/pc/en/Psi 0.11<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<urn:xmpp:dataforms:softwareinfo<ip_version<ipv4<ipv6<os<Mac<os_version<10.5.1<software<Psi<software_version<0.11<";
281        let mut expected = Vec::with_capacity(data.len());
282        expected.extend_from_slice(data);
283        let disco = DiscoInfoResult::try_from(elem).unwrap();
284        let caps = caps::compute_disco(&disco);
285        assert_eq!(caps, expected);
286
287        let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
288        assert_eq!(sha_1.hash, base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap());
289    }
290}