/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 | } |