sawp_tftp/
lib.rs

1//! A TFTP protocol parser. Given bytes and a [`sawp::parser::Direction`], it will
2//! attempt to parse the bytes and return a [`Message`]. The parser will
3//! inform the caller about what went wrong if no message is returned (see [`sawp::parser::Parse`]
4//! for details on possible return types). TFTP ignores Direction.
5//!
6//! The following protocol references were used to create this module:
7//!
8//! [TFTP Protocol (Revision 2)](https://tools.ietf.org/html/rfc1350)
9//!
10//! # Example
11//! ```
12//! use sawp::parser::{Direction, Parse};
13//! use sawp::error::Error;
14//! use sawp::error::ErrorKind;
15//! use sawp_tftp::{TFTP, Message};
16//!
17//! fn parse_bytes(input: &[u8]) -> std::result::Result<&[u8], Error> {
18//!     let parser = TFTP {};
19//!     let mut bytes = input;
20//!     while bytes.len() > 0 {
21//!         match parser.parse(bytes, Direction::Unknown) {
22//!             // The parser succeeded and returned the remaining bytes and the parsed TFTP message
23//!             Ok((rest, Some(message))) => {
24//!                 println!("TFTP message: {:?}", message);
25//!                 bytes = rest;
26//!             }
27//!             // The parser recognized that this might be TFTP and made some progress,
28//!             // but more bytes are needed
29//!             Ok((rest, None)) => return Ok(rest),
30//!             // The parser was unable to determine whether this was TFTP or not and more
31//!             // bytes are needed
32//!             Err(Error { kind: ErrorKind::Incomplete(_) }) => return Ok(bytes),
33//!             // The parser determined that this was not TFTP
34//!             Err(e) => return Err(e)
35//!         }
36//!     }
37//!
38//!     Ok(bytes)
39//! }
40//! ```
41
42#![allow(clippy::unneeded_field_pattern)]
43
44use sawp::error::{NomError, Result};
45use sawp::parser::{Direction, Parse};
46use sawp::probe::Probe;
47use sawp::protocol::Protocol;
48
49use num_enum::TryFromPrimitive;
50use std::convert::TryFrom;
51
52use nom::bytes::streaming::{tag, take_while};
53use nom::combinator::map_res;
54use nom::error::ErrorKind;
55use nom::number::streaming::be_u16;
56use nom::sequence::terminated;
57
58/// FFI structs and Accessors
59#[cfg(feature = "ffi")]
60mod ffi;
61
62#[cfg(feature = "ffi")]
63use sawp_ffi::GenerateFFI;
64
65/// The TFTP header of a packet contains the  opcode  associated  with
66/// that packet. TFTP supports five types of packets
67#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
68#[repr(u16)]
69pub enum OpCode {
70    ReadRequest = 1,
71    WriteRequest = 2,
72    Data = 3,
73    Acknowledgement = 4,
74    Error = 5,
75    OptionAcknowledgement = 6,
76}
77
78#[cfg_attr(feature = "ffi", derive(GenerateFFI), sawp_ffi(prefix = "sawp_tftp"))]
79#[derive(Debug, PartialEq, Eq)]
80pub enum Mode {
81    NetASCII,
82    Mail,
83    Octet,
84    Unknown(String),
85}
86
87///  The error code is an integer indicating the nature of the error.
88#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
89#[repr(u16)]
90pub enum ErrorCode {
91    NotDefined = 0,
92    FileNotFound = 1,
93    AccessViolation = 2,
94    DiskFull = 3,
95    IllegalTFTPOperation = 4,
96    UnknownTransferId = 5,
97    FileAlreadyExists = 6,
98    NoSuchUser = 7,
99    OptionRejected = 8,
100    Unknown = 65535,
101}
102
103#[cfg_attr(feature = "ffi", derive(GenerateFFI))]
104#[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_tftp"))]
105#[derive(Debug, PartialEq, Eq)]
106pub struct OptionExtension {
107    pub name: String,
108    pub value: String,
109}
110
111/// Represents the various types of TFTP Packets
112#[cfg_attr(feature = "ffi", derive(GenerateFFI), sawp_ffi(prefix = "sawp_tftp"))]
113#[derive(Debug, PartialEq, Eq)]
114pub enum Packet {
115    ReadWriteRequest {
116        filename: String,
117        mode: Mode,
118        options: Vec<OptionExtension>,
119    },
120    Data {
121        block_number: u16,
122        data: Vec<u8>,
123    },
124    Ack(u16),
125    Error {
126        raw_code: u16,
127        code: ErrorCode,
128        message: String,
129    },
130    OptAck(Vec<OptionExtension>),
131}
132
133/// Breakdown of the parsed TFTP bytes
134#[cfg_attr(feature = "ffi", derive(GenerateFFI), sawp_ffi(prefix = "sawp_tftp"))]
135#[derive(Debug, PartialEq, Eq)]
136pub struct Message {
137    #[cfg_attr(feature = "ffi", sawp_ffi(copy))]
138    pub op_code: OpCode,
139    pub packet: Packet,
140}
141
142#[derive(Debug)]
143pub struct TFTP {}
144
145impl<'a> Probe<'a> for TFTP {}
146
147impl Protocol<'_> for TFTP {
148    type Message = Message;
149
150    fn name() -> &'static str {
151        "tftp"
152    }
153}
154
155fn parse_options(input: &'_ [u8]) -> Result<(&'_ [u8], Vec<OptionExtension>)> {
156    let mut bytes = input;
157    let mut options: Vec<OptionExtension> = Vec::new();
158    while !bytes.is_empty() {
159        let (rest, name) = map_res(
160            terminated(take_while(|c| c != 0), tag(&[0])),
161            std::str::from_utf8,
162        )(bytes)?;
163        let (rest, value) = map_res(
164            terminated(take_while(|c| c != 0), tag(&[0])),
165            std::str::from_utf8,
166        )(rest)?;
167        options.push(OptionExtension {
168            name: name.into(),
169            value: value.into(),
170        });
171        bytes = rest;
172    }
173
174    Ok((bytes, options))
175}
176
177impl<'a> Parse<'a> for TFTP {
178    fn parse(
179        &self,
180        input: &'a [u8],
181        _direction: Direction,
182    ) -> Result<(&'a [u8], Option<Self::Message>)> {
183        let (input, op_code) = be_u16(input)?;
184        if let Ok(op_code) = OpCode::try_from(op_code) {
185            let (input, packet) = match op_code {
186                OpCode::ReadRequest | OpCode::WriteRequest => {
187                    let (input, filename) = map_res(
188                        terminated(take_while(|c| c != 0), tag(&[0])),
189                        std::str::from_utf8,
190                    )(input)?;
191                    let (input, mode) = map_res(
192                        terminated(take_while(|c| c != 0), tag(&[0])),
193                        std::str::from_utf8,
194                    )(input)?;
195                    let mode = match &mode.to_lowercase()[..] {
196                        "netascii" => Mode::NetASCII,
197                        "octet" => Mode::Octet,
198                        "mail" => Mode::Mail,
199                        _ => Mode::Unknown(mode.into()),
200                    };
201
202                    let (input, options) = match parse_options(input) {
203                        Ok((input, options)) => (input, options),
204                        _ => (input, Vec::new()),
205                    };
206
207                    (
208                        input,
209                        Packet::ReadWriteRequest {
210                            filename: filename.into(),
211                            mode,
212                            options,
213                        },
214                    )
215                }
216                OpCode::Data => {
217                    let (input, block_number) = be_u16(input)?;
218                    (
219                        &[] as &[u8],
220                        Packet::Data {
221                            block_number,
222                            data: input.into(),
223                        },
224                    )
225                }
226                OpCode::Acknowledgement => {
227                    let (input, block_number) = be_u16(input)?;
228                    (input, Packet::Ack(block_number))
229                }
230                OpCode::Error => {
231                    let (input, raw_code) = be_u16(input)?;
232                    let (input, message) = map_res(
233                        terminated(take_while(|c| c != 0), tag(&[0])),
234                        std::str::from_utf8,
235                    )(input)?;
236
237                    let code = ErrorCode::try_from(raw_code).unwrap_or(ErrorCode::Unknown);
238                    (
239                        input,
240                        Packet::Error {
241                            raw_code,
242                            code,
243                            message: message.into(),
244                        },
245                    )
246                }
247                OpCode::OptionAcknowledgement => match parse_options(input) {
248                    Ok((input, options)) => (input, Packet::OptAck(options)),
249                    _ => (input, Packet::OptAck(Vec::new())),
250                },
251            };
252            Ok((input, Some(Message { op_code, packet })))
253        } else {
254            Err(NomError::new(input, ErrorKind::IsA).into())
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use rstest::rstest;
263    use sawp::error;
264    use sawp::probe::Status;
265
266    #[test]
267    fn test_name() {
268        assert_eq!(TFTP::name(), "tftp");
269    }
270
271    #[rstest(
272        input,
273        expected,
274        case::empty(b"", Err(error::Error::incomplete_needed(2))),
275        case::hello_world(b"hello world", Err(NomError::new(b"hello world", ErrorKind::Tag).into())),
276        case::read(
277            &[
278                // OpCode: 1 (Read)
279                0x00, 0x01,
280                // Read
281                // Filename: log.txt
282                0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
283                // Mode: netascii
284                0x6e, 0x65, 0x74, 0x61, 0x73, 0x63, 0x69, 0x69, 0x00,
285            ],
286            Ok((&[] as &[u8],
287                Some(Message {
288                    op_code: OpCode::ReadRequest,
289                    packet: Packet::ReadWriteRequest {
290                        filename: String::from("log.txt"),
291                        mode: Mode::NetASCII,
292                        options: vec![],
293                    },
294                })))),
295        case::opt_read(
296            &[
297                // OpCode: 1 (Read)
298                0x00, 0x01,
299                // Read
300                // Filename: log.txt
301                0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
302                // Mode: netascii
303                0x6e, 0x65, 0x74, 0x61, 0x73, 0x63, 0x69, 0x69, 0x00,
304                // Options
305                // Option name: tsize
306                0x74, 0x73, 0x69, 0x7a, 0x65, 0x00,
307                // Option value: 0
308                0x30, 0x00,
309            ],
310            Ok((&[] as &[u8],
311                Some(Message {
312                    op_code: OpCode::ReadRequest,
313                    packet: Packet::ReadWriteRequest {
314                        filename: String::from("log.txt"),
315                        mode: Mode::NetASCII,
316                        options: vec![
317                            OptionExtension {
318                                name: String::from("tsize"),
319                                value: String::from("0"),
320                            }
321                        ],
322                    },
323                })))),
324        case::write(
325            &[
326                // OpCode: 2 (Write)
327                0x00, 0x02,
328                // Write
329                // Filename: log.txt
330                0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
331                // Mode: octet
332                0x4f, 0x63, 0x54, 0x65, 0x54, 0x00,
333            ],
334            Ok((&[] as &[u8],
335                Some(Message {
336                    op_code: OpCode::WriteRequest,
337                    packet: Packet::ReadWriteRequest {
338                        filename: String::from("log.txt"),
339                        mode: Mode::Octet,
340                        options: vec![],
341                    },
342                })))),
343        case::opt_write(
344            &[
345                // OpCode: 2 (Write)
346                0x00, 0x02,
347                // Write
348                // Filename: log.txt
349                0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
350                // Mode: octet
351                0x4f, 0x63, 0x54, 0x65, 0x54, 0x00,
352                // Options
353                // Option name: tsize
354                0x74, 0x73, 0x69, 0x7a, 0x65, 0x00,
355                // Option value: 0
356                0x30, 0x00,
357                // Option name: blksize
358                0x62, 0x6c, 0x6b, 0x73, 0x69, 0x7a, 0x65, 0x00,
359                // Option value: 1432
360                0x31, 0x34, 0x33, 0x32, 0x00,
361            ],
362            Ok((&[] as &[u8],
363                Some(Message {
364                    op_code: OpCode::WriteRequest,
365                    packet: Packet::ReadWriteRequest {
366                        filename: String::from("log.txt"),
367                        mode: Mode::Octet,
368                        options: vec![
369                            OptionExtension {
370                                name: String::from("tsize"),
371                                value: String::from("0"),
372                            },
373                            OptionExtension {
374                                name: String::from("blksize"),
375                                value: String::from("1432"),
376                            }
377                        ],
378                    },
379                })))),
380        case::unknown_mode(
381            &[
382                // OpCode: 2 (Write)
383                0x00, 0x02,
384                // Write
385                // Filename: log.txt
386                0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
387                // Mode: StRaNgEr
388                0x53, 0x74, 0x52, 0x61, 0x4e, 0x67, 0x45, 0x72, 0x00,
389            ],
390            Ok((&[] as &[u8],
391                Some(Message {
392                    op_code: OpCode::WriteRequest,
393                    packet: Packet::ReadWriteRequest {
394                        filename: String::from("log.txt"),
395                        mode: Mode::Unknown("StRaNgEr".into()),
396                        options: vec![],
397                    },
398                })))),
399        case::no_null(
400            &[
401                // OpCode: 2 (Write)
402                0x00, 0x02,
403                // Write
404                // Filename: log.txt
405                0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
406                // Mode: octet (no null termination)
407                0x4f, 0x63, 0x54, 0x65, 0x54,
408            ],
409            Err(error::Error::incomplete_needed(1))),
410        case::data(
411            &[
412                // OpCode: 3 (Data)
413                0x00, 0x03,
414                // Data
415                // Block Number: 12
416                0x00, 0x0c,
417                // Data
418                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
419            ],
420            Ok((&[] as &[u8],
421                Some(Message {
422                    op_code: OpCode::Data,
423                    packet: Packet::Data {
424                        block_number: 12,
425                        data: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
426                    },
427                })))),
428        case::ack(
429            &[
430                // OpCode: 4 (Acknowledgement)
431                0x00, 0x04,
432                // Block Number: 16,
433                0x00, 0x10,
434            ],
435            Ok((&[] as &[u8],
436                Some(Message {
437                   op_code: OpCode::Acknowledgement,
438                   packet: Packet::Ack(16),
439                })))),
440        case::opt_ack(
441            &[
442                // OpCode: 6 (OptionAcknowledgement)
443                0x00, 0x06,
444                // Options
445                // Option name: tsize
446                0x74, 0x73, 0x69, 0x7a, 0x65, 0x00,
447                // Option value: 0
448                0x30, 0x00,
449            ],
450            Ok((&[] as &[u8],
451                Some(Message {
452                    op_code: OpCode::OptionAcknowledgement,
453                    packet: Packet::OptAck(
454                        vec![OptionExtension {
455                            name: String::from("tsize"),
456                            value: String::from("0"),
457                        }],
458                    )
459                })))),
460        case::error(
461            &[
462                // OpCode: 5 (Error)
463                0x00, 0x05,
464                // Error 
465                // Code: 3 (DiskFull)
466                0x00, 0x03,
467                // Message: "Disk full"
468                0x44, 0x69, 0x73, 0x6b, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x00,
469            ],
470            Ok((&[] as &[u8],
471                Some(Message {
472                    op_code: OpCode::Error,
473                    packet: Packet::Error {
474                        raw_code: 3,
475                        code: ErrorCode::DiskFull,
476                        message: String::from("Disk full"),
477                    },
478                })))),
479    )]
480    fn test_parse(input: &[u8], expected: Result<(&[u8], Option<Message>)>) {
481        let tftp = TFTP {};
482        assert_eq!(tftp.parse(input, Direction::Unknown), expected);
483    }
484
485    #[rstest(
486        input,
487        expected,
488        case::empty(b"", Status::Incomplete),
489        case::hello_world(b"hello world", Status::Unrecognized),
490        case::header(
491        &[
492            // OpCode: 1 (Read)
493            0x00, 0x01,
494            // Read
495            // Filename: log.txt
496            0x6c, 0x6f, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x00,
497            // Mode: netascii
498            0x6e, 0x65, 0x74, 0x61, 0x73, 0x63, 0x69, 0x69, 0x00,
499        ],
500        Status::Recognized
501        ),
502    )]
503    fn test_probe(input: &[u8], expected: Status) {
504        let tftp = TFTP {};
505
506        assert_eq!(tftp.probe(input, Direction::Unknown), expected);
507    }
508}