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