Coverage Report

Created: 2026-09-06 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata8/rust/src/ftp/mod.rs
Line
Count
Source
1
/* Copyright (C) 2017 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
//! FTP parser and application layer module.
19
20
use nom7::bytes::complete::{tag, take_until};
21
use nom7::character::complete::{digit1, multispace0};
22
use nom7::combinator::{complete, map_res, opt, verify};
23
use nom7::sequence::{delimited, tuple};
24
use nom7::{Err, IResult};
25
use std;
26
use std::str;
27
use std::str::FromStr;
28
29
pub mod constant;
30
pub mod event;
31
pub mod ftp;
32
pub mod response;
33
34
// We transform an integer string into a i64, ignoring surrounding whitespaces
35
// We look for a digit suite, and try to convert it.
36
// If either str::from_utf8 or FromStr::from_str fail,
37
// we fallback to the parens parser defined above
38
44.8k
fn getu16(i: &[u8]) -> IResult<&[u8], u16> {
39
44.8k
    map_res(
40
44.8k
        map_res(delimited(multispace0, digit1, multispace0), str::from_utf8),
41
44.8k
        FromStr::from_str,
42
44.8k
    )(i)
43
44.8k
}
44
45
33.1k
fn parse_u16(i: &[u8]) -> IResult<&[u8], u16> {
46
33.1k
    map_res(map_res(digit1, str::from_utf8), u16::from_str)(i)
47
33.1k
}
48
49
// PORT 192,168,0,13,234,10
50
32.8k
fn ftp_active_port(i: &[u8]) -> IResult<&[u8], u16> {
51
32.8k
    let (i, _) = tag("PORT")(i)?;
52
24.3k
    let (i, _) = delimited(multispace0, digit1, multispace0)(i)?;
53
23.2k
    let (i, _) = tuple((
54
23.2k
        tag(","),
55
23.2k
        digit1,
56
23.2k
        tag(","),
57
23.2k
        digit1,
58
23.2k
        tag(","),
59
23.2k
        digit1,
60
23.2k
        tag(","),
61
23.2k
    ))(i)?;
62
18.1k
    let (i, part1) = verify(parse_u16, |&v| v <= u8::MAX as u16)(i)?;
63
16.3k
    let (i, _) = tag(",")(i)?;
64
14.9k
    let (i, part2) = verify(parse_u16, |&v| v <= u8::MAX as u16)(i)?;
65
2.72k
    Ok((i, part1 * 256 + part2))
66
32.8k
}
67
68
// 227 Entering Passive Mode (212,27,32,66,221,243).
69
16.2k
fn ftp_pasv_response(i: &[u8]) -> IResult<&[u8], u16> {
70
16.2k
    let (i, _) = tag("227")(i)?;
71
16.2k
    let (i, _) = take_until("(")(i)?;
72
11.7k
    let (i, _) = tag("(")(i)?;
73
11.7k
    let (i, _) = tuple((
74
11.7k
        digit1,
75
11.7k
        tag(","),
76
11.7k
        digit1,
77
11.7k
        tag(","),
78
11.7k
        digit1,
79
11.7k
        tag(","),
80
11.7k
        digit1,
81
11.7k
        tag(","),
82
11.7k
    ))(i)?;
83
5.31k
    let (i, part1) = verify(getu16, |&v| v <= u8::MAX as u16)(i)?;
84
4.22k
    let (i, _) = tag(",")(i)?;
85
2.68k
    let (i, part2) = verify(getu16, |&v| v <= u8::MAX as u16)(i)?;
86
    // may also be completed by a final point
87
1.99k
    let (i, _) = tag(")")(i)?;
88
1.54k
    let (i, _) = opt(complete(tag(".")))(i)?;
89
1.54k
    Ok((i, part1 * 256 + part2))
90
16.2k
}
91
92
#[no_mangle]
93
32.8k
pub unsafe extern "C" fn SCFTPParsePort(input: *const u8, len: u32) -> u16 {
94
32.8k
    if input.is_null() {
95
0
        return 0;
96
32.8k
    }
97
32.8k
    let buf = build_slice!(input, len as usize);
98
32.8k
    match ftp_active_port(buf) {
99
2.72k
        Ok((_, dport)) => {
100
2.72k
            return dport;
101
        }
102
0
        Err(Err::Incomplete(_)) => {
103
0
            SCLogDebug!("port incomplete: '{:?}'", buf);
104
0
        }
105
30.0k
        Err(_) => {
106
30.0k
            SCLogDebug!("port error on '{:?}'", buf);
107
30.0k
        }
108
    }
109
30.0k
    return 0;
110
32.8k
}
111
112
#[no_mangle]
113
16.2k
pub unsafe extern "C" fn SCFTPParsePortPasv(input: *const u8, len: u32) -> u16 {
114
16.2k
    if input.is_null() {
115
0
        return 0;
116
16.2k
    }
117
16.2k
    let buf = build_slice!(input, len as usize);
118
16.2k
    match ftp_pasv_response(buf) {
119
1.54k
        Ok((_, dport)) => {
120
1.54k
            return dport;
121
        }
122
0
        Err(Err::Incomplete(_)) => {
123
0
            SCLogDebug!("pasv incomplete: '{:?}'", String::from_utf8_lossy(buf));
124
0
        }
125
14.7k
        Err(_) => {
126
14.7k
            SCLogDebug!("pasv error on '{:?}'", String::from_utf8_lossy(buf));
127
14.7k
        }
128
    }
129
14.7k
    return 0;
130
16.2k
}
131
132
// 229 Entering Extended Passive Mode (|||48758|).
133
33.8k
pub fn ftp_epsv_response(i: &[u8]) -> IResult<&[u8], u16> {
134
33.8k
    let (i, _) = tag("229")(i)?;
135
33.8k
    let (i, _) = take_until("|||")(i)?;
136
29.4k
    let (i, _) = tag("|||")(i)?;
137
29.4k
    let (i, port) = getu16(i)?;
138
3.40k
    let (i, _) = tag("|)")(i)?;
139
2.33k
    let (i, _) = opt(complete(tag(".")))(i)?;
140
2.33k
    Ok((i, port))
141
33.8k
}
142
143
// EPRT |2|2a01:e34:ee97:b130:8c3e:45ea:5ac6:e301|41813|
144
17.2k
fn ftp_active_eprt(i: &[u8]) -> IResult<&[u8], u16> {
145
17.2k
    let (i, _) = tag("EPRT")(i)?;
146
14.3k
    let (i, _) = take_until("|")(i)?;
147
10.1k
    let (i, _) = tag("|")(i)?;
148
10.1k
    let (i, _) = take_until("|")(i)?;
149
9.19k
    let (i, _) = tag("|")(i)?;
150
9.19k
    let (i, _) = take_until("|")(i)?;
151
7.35k
    let (i, _) = tag("|")(i)?;
152
7.35k
    let (i, port) = getu16(i)?;
153
4.48k
    let (i, _) = tag("|")(i)?;
154
3.10k
    Ok((i, port))
155
17.2k
}
156
157
#[no_mangle]
158
17.2k
pub unsafe extern "C" fn SCFTPParsePortEprt(input: *const u8, len: u32) -> u16 {
159
17.2k
    if input.is_null() {
160
0
        return 0;
161
17.2k
    }
162
17.2k
    let buf = build_slice!(input, len as usize);
163
17.2k
    match ftp_active_eprt(buf) {
164
3.10k
        Ok((_, dport)) => {
165
3.10k
            return dport;
166
        }
167
0
        Err(Err::Incomplete(_)) => {
168
0
            SCLogDebug!("eprt incomplete: '{:?}'", String::from_utf8_lossy(buf));
169
0
        }
170
14.1k
        Err(_) => {
171
14.1k
            SCLogDebug!("epsv incomplete: '{:?}'", String::from_utf8_lossy(buf));
172
14.1k
        }
173
    }
174
14.1k
    return 0;
175
17.2k
}
176
#[no_mangle]
177
33.8k
pub unsafe extern "C" fn SCFTPParsePortEpsv(input: *const u8, len: u32) -> u16 {
178
33.8k
    if input.is_null() {
179
0
        return 0;
180
33.8k
    }
181
33.8k
    let buf = build_slice!(input, len as usize);
182
33.8k
    match ftp_epsv_response(buf) {
183
2.33k
        Ok((_, dport)) => {
184
2.33k
            return dport;
185
        }
186
0
        Err(Err::Incomplete(_)) => {
187
0
            SCLogDebug!("epsv incomplete: '{:?}'", String::from_utf8_lossy(buf));
188
0
        }
189
31.4k
        Err(_) => {
190
31.4k
            SCLogDebug!("epsv incomplete: '{:?}'", String::from_utf8_lossy(buf));
191
31.4k
        }
192
    }
193
31.4k
    return 0;
194
33.8k
}
195
196
#[cfg(test)]
197
mod test {
198
    use super::*;
199
200
    #[test]
201
    fn test_pasv_response_valid() {
202
        let port =
203
            ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,221,243).".as_bytes());
204
        assert_eq!(port, Ok((&b""[..], 56819)));
205
        let port_notdot =
206
            ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,221,243)".as_bytes());
207
        assert_eq!(port_notdot, Ok((&b""[..], 56819)));
208
209
        let port_epsv_dot =
210
            ftp_epsv_response("229 Entering Extended Passive Mode (|||48758|).".as_bytes());
211
        assert_eq!(port_epsv_dot, Ok((&b""[..], 48758)));
212
        let port_epsv_nodot =
213
            ftp_epsv_response("229 Entering Extended Passive Mode (|||48758|)".as_bytes());
214
        assert_eq!(port_epsv_nodot, Ok((&b""[..], 48758)));
215
    }
216
217
    #[test]
218
    fn test_active_eprt_valid() {
219
        let port =
220
            ftp_active_eprt("EPRT |2|2a01:e34:ee97:b130:8c3e:45ea:5ac6:e301|41813|".as_bytes());
221
        assert_eq!(port, Ok((&b""[..], 41813)));
222
    }
223
224
    #[test]
225
    fn test_active_port_valid() {
226
        let port = ftp_active_port("PORT 192,168,0,13,234,10".as_bytes());
227
        assert_eq!(port, Ok((&b""[..], 59914)));
228
    }
229
230
    // A port that is too large for a u16.
231
    #[test]
232
    fn test_pasv_response_too_large() {
233
        let port =
234
            ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,257,243).".as_bytes());
235
        assert!(port.is_err());
236
237
        let port =
238
            ftp_pasv_response("227 Entering Passive Mode (212,27,32,66,255,65535).".as_bytes());
239
        assert!(port.is_err());
240
    }
241
242
    #[test]
243
    fn test_active_eprt_too_large() {
244
        let port =
245
            ftp_active_eprt("EPRT |2|2a01:e34:ee97:b130:8c3e:45ea:5ac6:e301|81813|".as_bytes());
246
        assert!(port.is_err());
247
    }
248
249
    #[test]
250
    fn test_active_port_too_large() {
251
        let port = ftp_active_port("PORT 212,27,32,66,257,243".as_bytes());
252
        assert!(port.is_err());
253
254
        let port = ftp_active_port("PORT 212,27,32,66,255,65535".as_bytes());
255
        assert!(port.is_err());
256
    }
257
}