Coverage Report

Created: 2026-09-28 07:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata8/rust/src/ftp/ftp.rs
Line
Count
Source
1
/* Copyright (C) 2025 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
use std;
19
use std::ptr;
20
use std::ffi::{CStr, CString};
21
use std::os::raw::{c_char, c_void};
22
23
use crate::conf::{conf_get, get_memval};
24
use crate::core::*;
25
use crate::ftp::constant::*;
26
use lazy_static::lazy_static;
27
28
#[repr(C)]
29
pub struct DetectFtpModeData {
30
    pub active: bool,
31
}
32
33
#[repr(C)]
34
pub struct DetectFtpReplyReceivedData {
35
    pub received: bool,
36
}
37
38
#[repr(C)]
39
pub struct FtpCommand {
40
    command_name: CString,
41
    command: FtpRequestCommand,
42
    command_length: u8,
43
}
44
45
impl FtpCommand {
46
2.00k
    fn new(command_name: &str, command: FtpRequestCommand) -> FtpCommand {
47
2.00k
        let cstring = CString::new(command_name).unwrap();
48
2.00k
        let length = cstring.as_bytes().len();
49
2.00k
        FtpCommand {
50
2.00k
            command_name: cstring,
51
2.00k
            command,
52
2.00k
            command_length: length as u8,
53
2.00k
        }
54
2.00k
    }
55
}
56
57
lazy_static! {
58
    static ref FTP_COMMANDS: Vec<FtpCommand> = vec![
59
        FtpCommand::new("PORT", FtpRequestCommand::FTP_COMMAND_PORT),
60
        FtpCommand::new("EPRT", FtpRequestCommand::FTP_COMMAND_EPRT),
61
        FtpCommand::new("AUTH_TLS", FtpRequestCommand::FTP_COMMAND_AUTH_TLS),
62
        FtpCommand::new("PASV", FtpRequestCommand::FTP_COMMAND_PASV),
63
        FtpCommand::new("EPSV", FtpRequestCommand::FTP_COMMAND_EPSV),
64
        FtpCommand::new("RETR", FtpRequestCommand::FTP_COMMAND_RETR),
65
        FtpCommand::new("STOR", FtpRequestCommand::FTP_COMMAND_STOR),
66
        FtpCommand::new("ABOR", FtpRequestCommand::FTP_COMMAND_ABOR),
67
        FtpCommand::new("ACCT", FtpRequestCommand::FTP_COMMAND_ACCT),
68
        FtpCommand::new("ALLO", FtpRequestCommand::FTP_COMMAND_ALLO),
69
        FtpCommand::new("APPE", FtpRequestCommand::FTP_COMMAND_APPE),
70
        FtpCommand::new("CDUP", FtpRequestCommand::FTP_COMMAND_CDUP),
71
        FtpCommand::new("CHMOD", FtpRequestCommand::FTP_COMMAND_CHMOD),
72
        FtpCommand::new("CWD", FtpRequestCommand::FTP_COMMAND_CWD),
73
        FtpCommand::new("DELE", FtpRequestCommand::FTP_COMMAND_DELE),
74
        FtpCommand::new("HELP", FtpRequestCommand::FTP_COMMAND_HELP),
75
        FtpCommand::new("IDLE", FtpRequestCommand::FTP_COMMAND_IDLE),
76
        FtpCommand::new("LIST", FtpRequestCommand::FTP_COMMAND_LIST),
77
        FtpCommand::new("MAIL", FtpRequestCommand::FTP_COMMAND_MAIL),
78
        FtpCommand::new("MDTM", FtpRequestCommand::FTP_COMMAND_MDTM),
79
        FtpCommand::new("MKD", FtpRequestCommand::FTP_COMMAND_MKD),
80
        FtpCommand::new("MLFL", FtpRequestCommand::FTP_COMMAND_MLFL),
81
        FtpCommand::new("MLSD", FtpRequestCommand::FTP_COMMAND_MLSD),
82
        FtpCommand::new("MODE", FtpRequestCommand::FTP_COMMAND_MODE),
83
        FtpCommand::new("MRCP", FtpRequestCommand::FTP_COMMAND_MRCP),
84
        FtpCommand::new("MRSQ", FtpRequestCommand::FTP_COMMAND_MRSQ),
85
        FtpCommand::new("MSAM", FtpRequestCommand::FTP_COMMAND_MSAM),
86
        FtpCommand::new("MSND", FtpRequestCommand::FTP_COMMAND_MSND),
87
        FtpCommand::new("MSOM", FtpRequestCommand::FTP_COMMAND_MSOM),
88
        FtpCommand::new("NLST", FtpRequestCommand::FTP_COMMAND_NLST),
89
        FtpCommand::new("NOOP", FtpRequestCommand::FTP_COMMAND_NOOP),
90
        FtpCommand::new("PASS", FtpRequestCommand::FTP_COMMAND_PASS),
91
        FtpCommand::new("PWD", FtpRequestCommand::FTP_COMMAND_PWD),
92
        FtpCommand::new("QUIT", FtpRequestCommand::FTP_COMMAND_QUIT),
93
        FtpCommand::new("REIN", FtpRequestCommand::FTP_COMMAND_REIN),
94
        FtpCommand::new("REST", FtpRequestCommand::FTP_COMMAND_REST),
95
        FtpCommand::new("RMD", FtpRequestCommand::FTP_COMMAND_RMD),
96
        FtpCommand::new("RNFR", FtpRequestCommand::FTP_COMMAND_RNFR),
97
        FtpCommand::new("RNTO", FtpRequestCommand::FTP_COMMAND_RNTO),
98
        FtpCommand::new("SITE", FtpRequestCommand::FTP_COMMAND_SITE),
99
        FtpCommand::new("SIZE", FtpRequestCommand::FTP_COMMAND_SIZE),
100
        FtpCommand::new("SMNT", FtpRequestCommand::FTP_COMMAND_SMNT),
101
        FtpCommand::new("STAT", FtpRequestCommand::FTP_COMMAND_STAT),
102
        FtpCommand::new("STOU", FtpRequestCommand::FTP_COMMAND_STOU),
103
        FtpCommand::new("STRU", FtpRequestCommand::FTP_COMMAND_STRU),
104
        FtpCommand::new("SYST", FtpRequestCommand::FTP_COMMAND_SYST),
105
        FtpCommand::new("TYPE", FtpRequestCommand::FTP_COMMAND_TYPE),
106
        FtpCommand::new("UMASK", FtpRequestCommand::FTP_COMMAND_UMASK),
107
        FtpCommand::new("USER", FtpRequestCommand::FTP_COMMAND_USER),
108
        FtpCommand::new("UNKNOWN", FtpRequestCommand::FTP_COMMAND_UNKNOWN),
109
    ];
110
}
111
112
#[allow(non_snake_case)]
113
#[no_mangle]
114
65.5k
pub unsafe extern "C" fn SCGetFtpCommandInfo(
115
65.5k
    index: usize, name_ptr: *mut *const c_char, code_ptr: *mut u8, len_ptr: *mut u8,
116
65.5k
) -> bool {
117
65.5k
    if index <= FTP_COMMANDS.len() {
118
        unsafe {
119
65.5k
            if !name_ptr.is_null() {
120
8.29k
                *name_ptr = FTP_COMMANDS[index].command_name.as_ptr();
121
57.2k
            }
122
65.5k
            if !code_ptr.is_null() {
123
57.2k
                *code_ptr = FTP_COMMANDS[index].command as u8;
124
57.2k
            }
125
65.5k
            if !len_ptr.is_null() {
126
8.29k
                *len_ptr = FTP_COMMANDS[index].command_length;
127
57.2k
            }
128
        }
129
65.5k
        true
130
    } else {
131
0
        false
132
    }
133
65.5k
}
134
135
#[allow(non_snake_case)]
136
#[no_mangle]
137
40
pub unsafe extern "C" fn SCFTPSetMpmState(ctx: *const c_void) {
138
2.00k
    for index in 0..FTP_COMMANDS.len() {
139
2.00k
        let name_ptr = FTP_COMMANDS[index].command_name.as_ptr();
140
2.00k
        let len = FTP_COMMANDS[index].command_length;
141
2.00k
        if len > 0 {
142
2.00k
            MpmAddPatternCI(
143
2.00k
                ctx,
144
2.00k
                name_ptr,
145
2.00k
                len as u16,
146
2.00k
                0,
147
2.00k
                0,
148
2.00k
                index as u32,
149
2.00k
                index as u32,
150
2.00k
                0,
151
2.00k
            );
152
2.00k
        }
153
    }
154
40
}
155
156
#[repr(C)]
157
pub struct FtpTransferCmd {
158
    // Must be first -- required by app-layer expectation logic
159
    data_free: unsafe extern "C" fn(*mut c_void),
160
    pub flow_id: u64,
161
    pub file_name: *mut u8,
162
    pub file_len: u16,
163
    pub direction: u8,
164
    pub cmd: u8,
165
}
166
167
impl Default for FtpTransferCmd {
168
4.70k
    fn default() -> Self {
169
4.70k
        FtpTransferCmd {
170
4.70k
            flow_id: 0,
171
4.70k
            file_name: std::ptr::null_mut(),
172
4.70k
            file_len: 0,
173
4.70k
            direction: 0,
174
4.70k
            cmd: 0,
175
4.70k
            data_free: default_free_fn,
176
4.70k
        }
177
4.70k
    }
178
}
179
180
0
unsafe extern "C" fn default_free_fn(_ptr: *mut c_void) {}
181
impl FtpTransferCmd {
182
4.70k
    pub fn new() -> Self {
183
4.70k
        FtpTransferCmd {
184
4.70k
            ..Default::default()
185
4.70k
        }
186
4.70k
    }
187
}
188
189
#[no_mangle]
190
40
pub unsafe extern "C" fn SCFTPGetConfigValues(
191
40
    memcap: *mut u64, max_tx: *mut u32, max_line_len: *mut u32,
192
40
) {
193
40
    if let Some(val) = conf_get("app-layer.protocols.ftp.memcap") {
194
0
        if let Ok(v) = get_memval(val) {
195
0
            *memcap = v;
196
0
            SCLogConfig!("FTP memcap: {}", v);
197
        } else {
198
0
            SCLogWarning!(
199
0
                "Invalid value {} for ftp.memcap; defaulting to {}",
200
                val,
201
                *memcap
202
            );
203
        }
204
40
    }
205
40
    if let Some(val) = conf_get("app-layer.protocols.ftp.max-tx") {
206
0
        if let Ok(v) = val.parse::<u32>() {
207
0
            *max_tx = v;
208
0
            SCLogConfig!("FTP max tx: {}", v);
209
        } else {
210
0
            SCLogWarning!(
211
0
                "Invalid value {} for ftp.max-tx; defaulting to {}",
212
                val,
213
                *max_tx
214
            );
215
        }
216
40
    }
217
    // This value is often expressed with a unit suffix, e.g., 5kb, hence get_memval
218
40
    if let Some(val) = conf_get("app-layer.protocols.ftp.max-line-length") {
219
0
        if let Ok(v) = get_memval(val) {
220
0
            *max_line_len = v as u32;
221
0
            SCLogConfig!("FTP max line length: {}", v);
222
        } else {
223
0
            SCLogWarning!(
224
0
                "Invalid value {} for ftp.max-line-length; defaulting to {}",
225
                val,
226
                *max_line_len
227
            );
228
        }
229
40
    }
230
40
}
231
232
#[no_mangle]
233
24
pub unsafe extern "C" fn SCFTPParseReplyReceived(c_str: *const c_char) -> *mut DetectFtpReplyReceivedData {
234
24
    if c_str.is_null() {
235
0
        return ptr::null_mut();
236
24
    }
237
238
    // Convert C string to Rust string slice
239
24
    let Ok(input_str) = CStr::from_ptr(c_str).to_str() else {
240
0
        return ptr::null_mut();
241
    };
242
243
    // Check for case-insensitive match
244
24
    let received_val = match input_str.trim().to_ascii_lowercase().as_str() {
245
24
        "true" | "1" | "yes" | "on"  => true,
246
23
        "false" | "0" | "no" | "off"=> false,
247
21
        _ => return ptr::null_mut(), // invalid input
248
    };
249
250
    // Return a pointer to a heap-allocated struct
251
3
    let boxed = Box::new(DetectFtpReplyReceivedData { received: received_val });
252
3
    Box::into_raw(boxed)
253
24
}
254
255
#[no_mangle]
256
3
pub unsafe extern "C" fn SCFTPFreeReplyReceivedData(ptr: *mut DetectFtpReplyReceivedData) {
257
3
    if !ptr.is_null() {
258
3
        drop(Box::from_raw(ptr));
259
3
    }
260
3
}
261
#[no_mangle]
262
34
pub unsafe extern "C" fn SCFTPParseMode(c_str: *const c_char) -> *mut DetectFtpModeData {
263
34
    if c_str.is_null() {
264
0
        return ptr::null_mut();
265
34
    }
266
267
    // Convert C string to Rust string slice
268
34
    let Ok(input_str) = CStr::from_ptr(c_str).to_str() else {
269
0
        return ptr::null_mut();
270
    };
271
272
    // Check for case-insensitive match
273
34
    let is_active = match input_str.trim().to_ascii_lowercase().as_str() {
274
34
        "active" => true,
275
32
        "passive" => false,
276
29
        _ => return ptr::null_mut(), // invalid input
277
    };
278
279
    // Return a pointer to a heap-allocated struct
280
5
    let boxed = Box::new(DetectFtpModeData { active: is_active });
281
5
    Box::into_raw(boxed)
282
34
}
283
284
#[no_mangle]
285
5
pub unsafe extern "C" fn SCFTPFreeModeData(ptr: *mut DetectFtpModeData) {
286
5
    if !ptr.is_null() {
287
5
        drop(Box::from_raw(ptr));
288
5
    }
289
5
}
290
291
/// Returns *mut FtpTransferCmd
292
#[no_mangle]
293
4.70k
pub unsafe extern "C" fn SCFTPTransferCmdNew() -> *mut FtpTransferCmd {
294
    SCLogDebug!("allocating ftp transfer cmd");
295
4.70k
    let cmd = FtpTransferCmd::new();
296
4.70k
    Box::into_raw(Box::new(cmd))
297
4.70k
}
298
299
/// Params:
300
/// - transfer command: *mut FTPTransferCmd as void pointer
301
#[no_mangle]
302
4.68k
pub unsafe extern "C" fn SCFTPTransferCmdFree(cmd: *mut FtpTransferCmd) {
303
    SCLogDebug!("freeing ftp transfer cmd");
304
4.68k
    if !cmd.is_null() {
305
4.68k
        let _transfer_cmd = Box::from_raw(cmd);
306
4.68k
    }
307
4.68k
}