/src/suricata7/rust/src/bittorrent_dht/parser.rs
Line | Count | Source |
1 | | /* Copyright (C) 2021 Open Information Security Foundation |
2 | | * |
3 | | * You can copy, redistribute or modify this Program under the terms of |
4 | | * the GNU General Public License version 2 as published by the Free |
5 | | * Software Foundation. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * version 2 along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
15 | | * 02110-1301, USA. |
16 | | */ |
17 | | |
18 | | /*! Parses BitTorrent DHT specification BEP_0005 |
19 | | * <https://www.bittorrent.org/beps/bep_0005.html> !*/ |
20 | | |
21 | | // TODO: Custom error type, as we have bencode and nom errors, and may have an our application |
22 | | // specific errors as we finish off this parser. |
23 | | |
24 | | use crate::bittorrent_dht::bittorrent_dht::BitTorrentDHTTransaction; |
25 | | use bendy::decoding::{Decoder, Error, FromBencode, Object, ResultExt}; |
26 | | use nom7::bytes::complete::take; |
27 | | use nom7::number::complete::be_u16; |
28 | | use nom7::IResult; |
29 | | |
30 | | #[derive(Debug, Eq, PartialEq)] |
31 | | pub struct BitTorrentDHTRequest { |
32 | | /// q = * - 20 byte string, sender's node ID in network byte order |
33 | | pub id: Vec<u8>, |
34 | | /// q = find_node - target node ID |
35 | | pub target: Option<Vec<u8>>, |
36 | | /// q = get_peers/announce_peer - 20-byte info hash of target torrent |
37 | | pub info_hash: Option<Vec<u8>>, |
38 | | /// q = announce_peer - token key received from previous get_peers query |
39 | | pub token: Option<Vec<u8>>, |
40 | | /// q = announce_peer - 0 or 1, if 1 ignore provided port and |
41 | | /// use source port of UDP packet |
42 | | pub implied_port: Option<u8>, |
43 | | /// q = announce_peer - port on which peer will download torrent |
44 | | pub port: Option<u16>, |
45 | | } |
46 | | |
47 | | #[derive(Debug, Eq, PartialEq)] |
48 | | pub struct BitTorrentDHTResponse { |
49 | | /// q = * - 20 byte string, receiver's node ID in network byte order |
50 | | pub id: Vec<u8>, |
51 | | /// q = find_node/get_peers - compact node info for target node or |
52 | | /// K(8) closest good nodes in routing table |
53 | | pub nodes: Option<Vec<Node>>, |
54 | | pub nodes6: Option<Vec<Node>>, |
55 | | /// q = get_peers - list of compact peer infos |
56 | | pub values: Option<Vec<Peer>>, |
57 | | /// q = get_peers - token key required for sender's future |
58 | | /// announce_peer query |
59 | | pub token: Option<Vec<u8>>, |
60 | | } |
61 | | |
62 | | #[derive(Debug, Eq, PartialEq)] |
63 | | pub struct BitTorrentDHTError { |
64 | | /// integer representing the error code |
65 | | pub num: u16, |
66 | | /// string containing the error message |
67 | | pub msg: String, |
68 | | } |
69 | | |
70 | | #[derive(Debug, Eq, PartialEq)] |
71 | | pub struct Node { |
72 | | pub id: Vec<u8>, |
73 | | pub ip: Vec<u8>, |
74 | | pub port: u16, |
75 | | } |
76 | | |
77 | | #[derive(Debug, Eq, PartialEq)] |
78 | | pub struct Peer { |
79 | | pub ip: Vec<u8>, |
80 | | pub port: u16, |
81 | | } |
82 | | |
83 | | /// Parse IPv4 node structures. |
84 | 109k | pub fn parse_node(i: &[u8]) -> IResult<&[u8], Node> { |
85 | 109k | let (i, id) = take(20usize)(i)?; |
86 | 105k | let (i, ip) = take(4usize)(i)?; |
87 | 105k | let (i, port) = be_u16(i)?; |
88 | 105k | Ok(( |
89 | 105k | i, |
90 | 105k | Node { |
91 | 105k | id: id.to_vec(), |
92 | 105k | ip: ip.to_vec(), |
93 | 105k | port, |
94 | 105k | }, |
95 | 105k | )) |
96 | 109k | } |
97 | | |
98 | | /// Parse IPv6 node structures. |
99 | 42.7k | pub fn parse_node6(i: &[u8]) -> IResult<&[u8], Node> { |
100 | 42.7k | let (i, id) = take(20usize)(i)?; |
101 | 42.0k | let (i, ip) = take(16usize)(i)?; |
102 | 41.8k | let (i, port) = be_u16(i)?; |
103 | 41.6k | Ok(( |
104 | 41.6k | i, |
105 | 41.6k | Node { |
106 | 41.6k | id: id.to_vec(), |
107 | 41.6k | ip: ip.to_vec(), |
108 | 41.6k | port, |
109 | 41.6k | }, |
110 | 41.6k | )) |
111 | 42.7k | } |
112 | | |
113 | 822k | fn parse_peer(i: &[u8]) -> IResult<&[u8], Peer> { |
114 | 822k | let (i, ip) = if i.len() < 18 { |
115 | 822k | take(4usize)(i) |
116 | | } else { |
117 | 246 | take(16usize)(i) |
118 | 12 | }?; |
119 | 822k | let (i, port) = be_u16(i)?; |
120 | 822k | Ok(( |
121 | 822k | i, |
122 | 822k | Peer { |
123 | 822k | ip: ip.to_vec(), |
124 | 822k | port, |
125 | 822k | }, |
126 | 822k | )) |
127 | 822k | } |
128 | | |
129 | | impl FromBencode for BitTorrentDHTRequest { |
130 | | // Try to parse with a `max_depth` of one. |
131 | | // |
132 | | // The required max depth of a data structure is calculated as follows: |
133 | | // - every potential nesting level encoded as bencode dictionary or |
134 | | // list count as +1, |
135 | | // - everything else is ignored. |
136 | | // |
137 | | // struct BitTorrentDHTRequest { // encoded as dictionary (+1) |
138 | | // id: String, |
139 | | // target: Option<String>, |
140 | | // info_hash: Option<String>, |
141 | | // token: Option<String>, |
142 | | // implied_port: Option<u8>, |
143 | | // port: Option<u16>, |
144 | | // } |
145 | | const EXPECTED_RECURSION_DEPTH: usize = 1; |
146 | | |
147 | 17.7k | fn decode_bencode_object(object: Object) -> Result<Self, Error> |
148 | 17.7k | where |
149 | 17.7k | Self: Sized, |
150 | | { |
151 | 17.7k | let mut id = None; |
152 | 17.7k | let mut target = None; |
153 | 17.7k | let mut info_hash = None; |
154 | 17.7k | let mut token = None; |
155 | 17.7k | let mut implied_port = None; |
156 | 17.7k | let mut port = None; |
157 | | |
158 | 17.7k | let mut dict_dec = object.try_into_dictionary()?; |
159 | | |
160 | 61.6k | while let Some(pair) = dict_dec.next_pair()? { |
161 | 43.8k | match pair { |
162 | 43.8k | (b"id", value) => { |
163 | 16.7k | id = value.try_into_bytes().context("id").map(Some)?; |
164 | | } |
165 | 26.4k | (b"target", value) => { |
166 | 1.24k | target = value |
167 | 1.24k | .try_into_bytes() |
168 | 1.24k | .context("target") |
169 | 1.24k | .map(|v| Some(v.to_vec()))?; |
170 | | } |
171 | 20.4k | (b"info_hash", value) => { |
172 | 1.64k | info_hash = value |
173 | 1.64k | .try_into_bytes() |
174 | 1.64k | .context("info_hash") |
175 | 1.64k | .map(|v| Some(v.to_vec()))?; |
176 | | } |
177 | 16.3k | (b"token", value) => { |
178 | 1.82k | token = value |
179 | 1.82k | .try_into_bytes() |
180 | 1.82k | .context("token") |
181 | 1.82k | .map(|v| Some(v.to_vec()))?; |
182 | | } |
183 | 11.3k | (b"implied_port", value) => { |
184 | 932 | implied_port = u8::decode_bencode_object(value) |
185 | 932 | .context("implied_port") |
186 | 932 | .map(Some)? |
187 | | } |
188 | 2.43k | (b"port", value) => { |
189 | 569 | port = u16::decode_bencode_object(value) |
190 | 569 | .context("port") |
191 | 569 | .map(Some)? |
192 | | } |
193 | 20.9k | (_unknown_field, _) => {} |
194 | | } |
195 | | } |
196 | | |
197 | 15.9k | let id = id.ok_or_else(|| Error::missing_field("id"))?; |
198 | | |
199 | 15.8k | Ok(BitTorrentDHTRequest { |
200 | 15.8k | id: id.to_vec(), |
201 | 15.8k | target, |
202 | 15.8k | info_hash, |
203 | 15.8k | token, |
204 | 15.8k | implied_port, |
205 | 15.8k | port, |
206 | 15.8k | }) |
207 | 17.7k | } |
208 | | } |
209 | | |
210 | | impl FromBencode for BitTorrentDHTResponse { |
211 | | // Try to parse with a `max_depth` of two. |
212 | | // |
213 | | // The required max depth of a data structure is calculated as follows: |
214 | | // - every potential nesting level encoded as bencode dictionary or |
215 | | // list count as +1, |
216 | | // - everything else is ignored. |
217 | | // |
218 | | // struct BitTorrentDHTResponse { // encoded as dictionary (+1) |
219 | | // id: String, |
220 | | // nodes: Option<String>, |
221 | | // values: Option<Vec<String>>, // if present, encoded as list (+1) |
222 | | // token: Option<String>, |
223 | | // } |
224 | | const EXPECTED_RECURSION_DEPTH: usize = 2; |
225 | | |
226 | 13.8k | fn decode_bencode_object(object: Object) -> Result<Self, Error> |
227 | 13.8k | where |
228 | 13.8k | Self: Sized, |
229 | | { |
230 | 13.8k | let mut id = None; |
231 | 13.8k | let mut nodes = None; |
232 | 13.8k | let mut nodes6 = None; |
233 | 13.8k | let mut values = vec![]; |
234 | 13.8k | let mut token = None; |
235 | | |
236 | 13.8k | let mut dict_dec = object.try_into_dictionary()?; |
237 | | |
238 | 42.2k | while let Some(pair) = dict_dec.next_pair()? { |
239 | 28.5k | match pair { |
240 | 28.5k | (b"id", value) => { |
241 | 12.9k | id = value.try_into_bytes().context("id").map(Some)?; |
242 | | } |
243 | 14.9k | (b"nodes", value) => { |
244 | 3.90k | let (_, decoded_nodes) = |
245 | 3.91k | nom7::multi::many0(parse_node)(value.try_into_bytes().context("nodes")?) |
246 | 3.90k | .map_err(|_| Error::malformed_content("nodes.node"))?; |
247 | 3.90k | if !decoded_nodes.is_empty() { |
248 | 542 | nodes = Some(decoded_nodes); |
249 | 3.36k | } |
250 | | } |
251 | 6.49k | (b"nodes6", value) => { |
252 | 1.07k | let (_, decoded_nodes) = |
253 | 1.07k | nom7::multi::many0(parse_node6)(value.try_into_bytes().context("nodes6")?) |
254 | 1.07k | .map_err(|_| Error::malformed_content("nodes6.nodes6"))?; |
255 | 1.07k | if !decoded_nodes.is_empty() { |
256 | 479 | nodes6 = Some(decoded_nodes); |
257 | 594 | } |
258 | | } |
259 | 1.52k | (b"values", value) => { |
260 | 1.52k | if let Object::List(mut list) = value { |
261 | 823k | while let Some(entry) = list.next_object()? { |
262 | 822k | let (_, peer) = |
263 | 822k | parse_peer(entry.try_into_bytes().context("values.entry")?) |
264 | 822k | .map_err(|_| Error::malformed_content("values.entry.peer"))?; |
265 | 822k | values.push(peer); |
266 | | } |
267 | 670 | } |
268 | | } |
269 | 1.01k | (b"token", value) => { |
270 | 1.01k | token = value |
271 | 1.01k | .try_into_bytes() |
272 | 1.01k | .context("token") |
273 | 1.01k | .map(|v| Some(v.to_vec()))?; |
274 | | } |
275 | 8.11k | (_unknown_field, _) => {} |
276 | | } |
277 | | } |
278 | | |
279 | 12.1k | let id = id.ok_or_else(|| Error::missing_field("id"))?; |
280 | | |
281 | | Ok(BitTorrentDHTResponse { |
282 | 12.0k | id: id.to_vec(), |
283 | 12.0k | nodes, |
284 | 12.0k | nodes6, |
285 | 12.0k | values: if values.is_empty() { |
286 | 11.4k | None |
287 | | } else { |
288 | 587 | Some(values) |
289 | | }, |
290 | 12.0k | token, |
291 | | }) |
292 | 13.8k | } |
293 | | } |
294 | | |
295 | | impl FromBencode for BitTorrentDHTError { |
296 | | // Try to parse with a `max_depth` of one. |
297 | | // |
298 | | // The required max depth of a data structure is calculated as follows: |
299 | | // - every potential nesting level encoded as bencode dictionary or |
300 | | // list count as +1, |
301 | | // - everything else is ignored. |
302 | | // |
303 | | // struct BitTorrentDHTError { // encoded as dictionary (+1) |
304 | | // num: u16, |
305 | | // msg: String, |
306 | | // } |
307 | | const EXPECTED_RECURSION_DEPTH: usize = 1; |
308 | | |
309 | 86.8k | fn decode_bencode_object(object: Object) -> Result<Self, Error> |
310 | 86.8k | where |
311 | 86.8k | Self: Sized, |
312 | | { |
313 | 86.8k | let mut num = None; |
314 | 86.8k | let mut msg = None; |
315 | | |
316 | 86.8k | let mut list_dec = object.try_into_list()?; |
317 | | |
318 | 717k | while let Some(object) = list_dec.next_object()? { |
319 | 630k | match object { |
320 | | Object::Integer(_) => { |
321 | 88.0k | num = u16::decode_bencode_object(object) |
322 | 88.0k | .context("num") |
323 | 88.0k | .map(Some)?; |
324 | | } |
325 | | Object::Bytes(_) => { |
326 | 159k | msg = String::decode_bencode_object(object) |
327 | 159k | .context("msg") |
328 | 159k | .map(Some)?; |
329 | | } |
330 | 383k | _ => {} |
331 | | } |
332 | | } |
333 | | |
334 | 86.0k | let num = num.ok_or_else(|| Error::missing_field("num"))?; |
335 | 86.0k | let msg = msg.ok_or_else(|| Error::missing_field("msg"))?; |
336 | | |
337 | 86.0k | Ok(BitTorrentDHTError { num, msg }) |
338 | 86.8k | } |
339 | | } |
340 | | |
341 | 109k | pub fn parse_bittorrent_dht_packet( |
342 | 109k | bytes: &[u8], tx: &mut BitTorrentDHTTransaction, |
343 | 109k | ) -> Result<(), Error> { |
344 | | // Try to parse with a `max_depth` of three. |
345 | | // |
346 | | // The required max depth of a data structure is calculated as follows: |
347 | | // - every potential nesting level encoded as bencode dictionary or |
348 | | // list count as +1, |
349 | | // - everything else is ignored. |
350 | | // |
351 | | // - Outer packet is a dictionary (+1) |
352 | | // - Max depth of child within dictionary is a BitTorrentDHTResponse (+2) |
353 | 109k | let mut decoder = Decoder::new(bytes).with_max_depth(3); |
354 | 109k | let object = decoder.next_object()?; |
355 | | |
356 | 109k | let mut packet_type = None; |
357 | 109k | let mut query_type = None; |
358 | 109k | let mut query_arguments = None; |
359 | 109k | let mut response = None; |
360 | 109k | let mut error = None; |
361 | 109k | let mut transaction_id = None; |
362 | 109k | let mut client_version = None; |
363 | | |
364 | 109k | let mut dict_dec = object |
365 | 109k | .ok_or_else(|| Error::unexpected_token("Dict", "EOF"))? |
366 | 109k | .try_into_dictionary()?; |
367 | | |
368 | 439k | while let Some(pair) = dict_dec.next_pair()? { |
369 | 335k | match pair { |
370 | 335k | (b"y", value) => { |
371 | | // q (query) vs r (response) vs e (error) |
372 | 102k | packet_type = String::decode_bencode_object(value) |
373 | 102k | .context("packet_type") |
374 | 102k | .map(Some)?; |
375 | | } |
376 | 8.16k | (b"q", value) => { |
377 | | // query type found |
378 | 8.16k | query_type = String::decode_bencode_object(value) |
379 | 8.16k | .context("query_type") |
380 | 8.16k | .map(Some)?; |
381 | | } |
382 | 17.7k | (b"a", value) => { |
383 | | // query arguments found |
384 | 17.7k | query_arguments = BitTorrentDHTRequest::decode_bencode_object(value) |
385 | 17.7k | .context("query_arguments") |
386 | 17.7k | .map(Some)?; |
387 | | } |
388 | 13.8k | (b"r", value) => { |
389 | | // response found |
390 | 13.8k | response = BitTorrentDHTResponse::decode_bencode_object(value) |
391 | 13.8k | .context("response") |
392 | 13.8k | .map(Some)?; |
393 | | } |
394 | 86.8k | (b"e", value) => { |
395 | | // error found |
396 | 86.8k | error = BitTorrentDHTError::decode_bencode_object(value) |
397 | 86.8k | .context("error") |
398 | 86.8k | .map(Some)?; |
399 | | } |
400 | 102k | (b"t", value) => { |
401 | | // transaction id found |
402 | 102k | transaction_id = value.try_into_bytes().context("transaction_id").map(Some)?; |
403 | | } |
404 | 1.68k | (b"v", value) => { |
405 | | // client version string found |
406 | 1.68k | client_version = value |
407 | 1.68k | .try_into_bytes() |
408 | 1.68k | .context("client_version") |
409 | 1.68k | .map(|v| Some(v.to_vec()))?; |
410 | | } |
411 | 1.76k | (_unknown_field, _) => {} |
412 | | } |
413 | | } |
414 | | |
415 | 102k | if let Some(t) = packet_type { |
416 | 102k | match t.as_str() { |
417 | 102k | "q" => { |
418 | 5.99k | tx.request_type = |
419 | 5.99k | Some(query_type.ok_or_else(|| Error::missing_field("query_type"))?); |
420 | 5.99k | tx.request = |
421 | 5.99k | Some(query_arguments.ok_or_else(|| Error::missing_field("query_arguments"))?); |
422 | | } |
423 | 96.2k | "r" => { |
424 | 11.0k | tx.response = Some(response.ok_or_else(|| Error::missing_field("response"))?); |
425 | | } |
426 | 85.2k | "e" => { |
427 | 85.1k | tx.error = Some(error.ok_or_else(|| Error::missing_field("error"))?); |
428 | | } |
429 | 53 | v => { |
430 | 53 | return Err(Error::unexpected_token("packet_type q, r, or e", v)); |
431 | | } |
432 | | } |
433 | | } else { |
434 | 46 | return Err(Error::missing_field("packet_type")); |
435 | | } |
436 | | |
437 | 102k | tx.transaction_id = transaction_id |
438 | 102k | .ok_or_else(|| Error::missing_field("transaction_id"))? |
439 | 102k | .to_vec(); |
440 | | // Client version string is an optional field |
441 | 102k | tx.client_version = client_version; |
442 | | |
443 | 102k | Ok(()) |
444 | 109k | } |
445 | | |
446 | | #[cfg(test)] |
447 | | #[allow(clippy::unused_unit)] |
448 | | mod tests { |
449 | | use super::*; |
450 | | use crate::core::Direction; |
451 | | use test_case::test_case; |
452 | | |
453 | | #[test_case( |
454 | | b"d2:id20:abcdefghij0123456789e", |
455 | | BitTorrentDHTRequest { id: b"abcdefghij0123456789".to_vec(), implied_port: None, info_hash: None, port: None, token: None, target: None } ; |
456 | | "test request from bencode 2")] |
457 | | #[test_case( |
458 | | b"d2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e", |
459 | | BitTorrentDHTRequest { id: b"abcdefghij0123456789".to_vec(), implied_port: None, info_hash: Some(b"mnopqrstuvwxyz123456".to_vec()), port: None, token: None, target: None } ; |
460 | | "test request from bencode 4")] |
461 | | fn test_request_from_bencode(encoded: &[u8], expected: BitTorrentDHTRequest) { |
462 | | let decoded = BitTorrentDHTRequest::from_bencode(encoded).unwrap(); |
463 | | assert_eq!(expected, decoded); |
464 | | } |
465 | | |
466 | | #[test_case( |
467 | | b"d12:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe", |
468 | | "Error: missing field: id" ; |
469 | | "test request from bencode err 1")] |
470 | | #[test_case( |
471 | | b"d2:id20:abcdefghij012345678912:implied_porti9999e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe", |
472 | | "Error: malformed content discovered in implied_port" ; |
473 | | "test request from bencode err 2")] |
474 | | #[test_case( |
475 | | b"d2:id20:abcdefghij012345678912:implied_porti-1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe", |
476 | | "Error: malformed content discovered in implied_port" ; |
477 | | "test request from bencode err 3")] |
478 | | #[test_case( |
479 | | b"d2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti9999999e5:token8:aoeusnthe", |
480 | | "Error: malformed content discovered in port" ; |
481 | | "test request from bencode err 4")] |
482 | | #[test_case( |
483 | | b"d2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti-1e5:token8:aoeusnthe", |
484 | | "Error: malformed content discovered in port" ; |
485 | | "test request from bencode err 5")] |
486 | | #[test_case( |
487 | | b"i123e", |
488 | | "Error: discovered Dict but expected Num" ; |
489 | | "test request from bencode err 6")] |
490 | | fn test_request_from_bencode_err(encoded: &[u8], expected_error: &str) { |
491 | | let err = BitTorrentDHTRequest::from_bencode(encoded).unwrap_err(); |
492 | | assert_eq!(expected_error, err.to_string()); |
493 | | } |
494 | | |
495 | | #[test_case( |
496 | | b"d5:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee", |
497 | | "Error: missing field: id" ; |
498 | | "test response from bencode err 1")] |
499 | | #[test_case( |
500 | | b"i123e", |
501 | | "Error: discovered Dict but expected Num" ; |
502 | | "test response from bencode err 2")] |
503 | | fn test_response_from_bencode_err(encoded: &[u8], expected_error: &str) { |
504 | | let err = BitTorrentDHTResponse::from_bencode(encoded).unwrap_err(); |
505 | | assert_eq!(expected_error, err.to_string()); |
506 | | } |
507 | | |
508 | | #[test_case( |
509 | | b"li201e23:A Generic Error Ocurrede", |
510 | | BitTorrentDHTError { num: 201u16, msg: "A Generic Error Ocurred".to_string() } ; |
511 | | "test error from bencode 1")] |
512 | | #[test_case( |
513 | | b"li202e12:Server Errore", |
514 | | BitTorrentDHTError { num: 202u16, msg: "Server Error".to_string() } ; |
515 | | "test error from bencode 2")] |
516 | | #[test_case( |
517 | | b"li203e14:Protocol Errore", |
518 | | BitTorrentDHTError { num: 203u16, msg: "Protocol Error".to_string() } ; |
519 | | "test error from bencode 3")] |
520 | | #[test_case( |
521 | | b"li204e14:Method Unknowne", |
522 | | BitTorrentDHTError { num: 204u16, msg: "Method Unknown".to_string() } ; |
523 | | "test error from bencode 4")] |
524 | | fn test_error_from_bencode(encoded: &[u8], expected: BitTorrentDHTError) { |
525 | | let decoded = BitTorrentDHTError::from_bencode(encoded).unwrap(); |
526 | | assert_eq!(expected, decoded); |
527 | | } |
528 | | |
529 | | #[test_case( |
530 | | b"l23:A Generic Error Ocurrede", |
531 | | "Error: missing field: num" ; |
532 | | "test error from bencode err 1")] |
533 | | #[test_case( |
534 | | b"li201ee", |
535 | | "Error: missing field: msg" ; |
536 | | "test error from bencode err 2")] |
537 | | #[test_case( |
538 | | b"li999999ee", |
539 | | "Error: malformed content discovered in num" ; |
540 | | "test error from bencode err 3")] |
541 | | #[test_case( |
542 | | b"li-1ee", |
543 | | "Error: malformed content discovered in num" ; |
544 | | "test error from bencode err 4")] |
545 | | #[test_case( |
546 | | b"i123e", |
547 | | "Error: discovered List but expected Num" ; |
548 | | "test error from bencode err 5")] |
549 | | fn test_error_from_bencode_err(encoded: &[u8], expected_error: &str) { |
550 | | let err = BitTorrentDHTError::from_bencode(encoded).unwrap_err(); |
551 | | assert_eq!(expected_error, err.to_string()); |
552 | | } |
553 | | |
554 | | #[test_case( |
555 | | b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:v4:UT011:y1:qe", |
556 | | Some("ping".to_string()), |
557 | | Some(BitTorrentDHTRequest { id: b"abcdefghij0123456789".to_vec(), implied_port: None, info_hash: None, port: None, token: None, target: None }), |
558 | | None, |
559 | | None, |
560 | | b"aa".to_vec(), |
561 | | Some(b"UT01".to_vec()) ; |
562 | | "test parse bittorrent dht packet 1" |
563 | | )] |
564 | | #[test_case( |
565 | | b"d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:v4:UT011:y1:ee", |
566 | | None, |
567 | | None, |
568 | | None, |
569 | | Some(BitTorrentDHTError { num: 201u16, msg: "A Generic Error Ocurred".to_string() }), |
570 | | b"aa".to_vec(), |
571 | | Some(b"UT01".to_vec()) ; |
572 | | "test parse bittorrent dht packet 3" |
573 | | )] |
574 | | fn test_parse_bittorrent_dht_packet( |
575 | | encoded: &[u8], request_type: Option<String>, |
576 | | expected_request: Option<BitTorrentDHTRequest>, |
577 | | expected_response: Option<BitTorrentDHTResponse>, |
578 | | expected_error: Option<BitTorrentDHTError>, expected_transaction_id: Vec<u8>, |
579 | | expected_client_version: Option<Vec<u8>>, |
580 | | ) { |
581 | | let mut tx = BitTorrentDHTTransaction::new(Direction::ToServer); |
582 | | parse_bittorrent_dht_packet(encoded, &mut tx).unwrap(); |
583 | | assert_eq!(request_type, tx.request_type); |
584 | | assert_eq!(expected_request, tx.request); |
585 | | assert_eq!(expected_response, tx.response); |
586 | | assert_eq!(expected_error, tx.error); |
587 | | assert_eq!(expected_transaction_id, tx.transaction_id); |
588 | | assert_eq!(expected_client_version, tx.client_version); |
589 | | } |
590 | | |
591 | | #[test_case( |
592 | | b"", |
593 | | "Error: discovered Dict but expected EOF" ; |
594 | | "test parse bittorrent dht packet err 1" |
595 | | )] |
596 | | #[test_case( |
597 | | b"li2123ei321ee", |
598 | | "Error: discovered Dict but expected List" ; |
599 | | "test parse bittorrent dht packet err 2" |
600 | | )] |
601 | | #[test_case( |
602 | | b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aae", |
603 | | "Error: missing field: packet_type" ; |
604 | | "test parse bittorrent dht packet err 3" |
605 | | )] |
606 | | #[test_case( |
607 | | b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:Fe", |
608 | | "Error: discovered packet_type q, r, or e but expected F" ; |
609 | | "test parse bittorrent dht packet err 4" |
610 | | )] |
611 | | #[test_case( |
612 | | b"d1:ad2:id20:abcdefghij0123456789e1:t2:aa1:y1:qe", |
613 | | "Error: missing field: query_type" ; |
614 | | "test parse bittorrent dht packet err 5" |
615 | | )] |
616 | | #[test_case( |
617 | | b"d1:q4:ping1:t2:aa1:y1:qe", |
618 | | "Error: missing field: query_arguments" ; |
619 | | "test parse bittorrent dht packet err 6" |
620 | | )] |
621 | | #[test_case( |
622 | | b"d1:t2:aa1:y1:re", |
623 | | "Error: missing field: response" ; |
624 | | "test parse bittorrent dht packet err 7" |
625 | | )] |
626 | | #[test_case( |
627 | | b"d1:t2:aa1:y1:ee", |
628 | | "Error: missing field: error" ; |
629 | | "test parse bittorrent dht packet err 8" |
630 | | )] |
631 | | #[test_case( |
632 | | b"d1:ade1:q4:ping1:t2:aa1:y1:qe", |
633 | | "Error: missing field: id in query_arguments" ; |
634 | | "test parse bittorrent dht packet err 9" |
635 | | )] |
636 | | #[test_case( |
637 | | b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:y1:qe", |
638 | | "Error: missing field: transaction_id" ; |
639 | | "test parse bittorrent dht packet err 10" |
640 | | )] |
641 | | fn test_parse_bittorrent_dht_packet_err(encoded: &[u8], expected_error: &str) { |
642 | | let mut tx = BitTorrentDHTTransaction::new(Direction::ToServer); |
643 | | let err = parse_bittorrent_dht_packet(encoded, &mut tx).unwrap_err(); |
644 | | assert_eq!(expected_error, err.to_string()); |
645 | | } |
646 | | |
647 | | #[test] |
648 | | fn test_parse_node() { |
649 | | let bytes = b"aaaaaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x01"; |
650 | | let (_rem, node) = parse_node(bytes).unwrap(); |
651 | | assert_eq!(node.id, b"aaaaaaaaaaaaaaaaaaaa"); |
652 | | assert_eq!(node.ip, b"\x00\x00\x00\x00"); |
653 | | assert_eq!(node.port, 1); |
654 | | |
655 | | // Short one byte. |
656 | | let bytes = b"aaaaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x01"; |
657 | | assert!(parse_node(bytes).is_err()); |
658 | | |
659 | | // Has remaining bytes. |
660 | | let bytes = b"aaaaaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x01bb"; |
661 | | let (rem, _node) = parse_node(bytes).unwrap(); |
662 | | assert_eq!(rem, b"bb"); |
663 | | } |
664 | | } |