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