From dfc896e2a7a19f67df46242ae2d2b8c5a3856cc3 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Fri, 21 Mar 2025 09:57:38 -0400 Subject: [PATCH] app/ftp: Move FTP response handling to rust Move handling of FTP responses to Rust to improve support for FTP keyword matching. Parsing the response line when encountered simplifies multi-buffer matching and metadata output. Issue: 4082 --- rust/src/ftp/mod.rs | 1 + rust/src/ftp/response.rs | 213 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 rust/src/ftp/response.rs diff --git a/rust/src/ftp/mod.rs b/rust/src/ftp/mod.rs index b7b74732c9..479126971f 100644 --- a/rust/src/ftp/mod.rs +++ b/rust/src/ftp/mod.rs @@ -29,6 +29,7 @@ use std::str::FromStr; pub mod constant; pub mod event; pub mod ftp; +pub mod response; // We transform an integer string into a i64, ignoring surrounding whitespaces // We look for a digit suite, and try to convert it. diff --git a/rust/src/ftp/response.rs b/rust/src/ftp/response.rs new file mode 100644 index 0000000000..1dc2e02e1b --- /dev/null +++ b/rust/src/ftp/response.rs @@ -0,0 +1,213 @@ +/* Copyright (C) 2025 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use std::ffi::CString; +use std::os::raw::c_char; +use std::ptr; +use std::slice; + +#[repr(C)] +pub struct FTPResponseLine { + code: *mut u8, // Response code as a string (may be null) + response: *mut u8, // Response string + length: usize, // Length of the response string + truncated: bool, // Uses TX/state value. + total_size: usize, // Total allocated size in bytes +} + +/// Parses a single FTP response line and returns an FTPResponseLine struct. +/// Handles response lines like: +/// - (single response) "530 Login incorrect" +/// - (single response, no code) "Login incorrect" +fn parse_response_line(input: &str) -> Option { + // Find the first complete response line (delimited by `\r\n`) + let mut split = input.splitn(2, "\r\n"); + let response_line = split.next().unwrap_or("").trim_end(); + + if response_line.is_empty() { + return None; // Ignore empty input + } + + // Extract response code as a string + let mut parts = response_line.splitn(2, ' '); + let (code, response) = match (parts.next(), parts.next()) { + (Some(num_str), Some(rest)) + if num_str.len() == 3 && num_str.chars().all(|c| c.is_ascii_digit()) => + { + (num_str.to_string(), rest) + } + _ => ("".to_string(), response_line), // No valid numeric code found + }; + + // Convert response and code to C strings + let c_code = CString::new(code).ok()?; + let c_response = CString::new(response).ok()?; + + // Compute memory usage + let total_size = std::mem::size_of::() + + c_code.as_bytes_with_nul().len() + + c_response.as_bytes_with_nul().len(); + + Some(FTPResponseLine { + code: c_code.into_raw() as *mut u8, + response: c_response.into_raw() as *mut u8, + length: response.len(), + truncated: false, + total_size, + }) +} + +/// Parses an FTP response string and returns a pointer to an `FTPResponseLine` struct. +#[no_mangle] +pub unsafe extern "C" fn SCFTPParseResponseLine( + input: *const c_char, length: usize, +) -> *mut FTPResponseLine { + if input.is_null() || length == 0 { + return ptr::null_mut(); + } + + let slice = slice::from_raw_parts(input as *const u8, length); + + let input_str = String::from_utf8_lossy(slice); + + match parse_response_line(&input_str) { + Some(response) => Box::into_raw(Box::new(response)), + None => ptr::null_mut(), + } +} + +/// Frees the memory allocated for an `FTPResponseLine` struct. +#[no_mangle] +pub unsafe extern "C" fn SCFTPFreeResponseLine(response: *mut FTPResponseLine) { + if response.is_null() { + return; + } + + let response = Box::from_raw(response); + + if !response.code.is_null() { + let _ = CString::from_raw(response.code as *mut c_char); + } + + if !response.response.is_null() { + let _ = CString::from_raw(response.response as *mut c_char); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CStr; + + #[test] + fn test_parse_valid_response() { + let input = "220 Welcome to FTP\r\n"; + let parsed = parse_response_line(input).unwrap(); + + let code_cstr = unsafe { CStr::from_ptr(parsed.code as *mut c_char) }; + let code_str = code_cstr.to_str().unwrap(); + assert_eq!(code_str, "220"); + let response_cstr = unsafe { CStr::from_ptr(parsed.response as *mut c_char) }; + let response_str = response_cstr.to_str().unwrap(); + assert_eq!(response_str, "Welcome to FTP"); + assert_eq!(parsed.length, "Welcome to FTP".len()); + } + + #[test] + fn test_parse_response_without_code() { + let input = "Some random text\r\n"; + let parsed = parse_response_line(input).unwrap(); + + let code_cstr = unsafe { CStr::from_ptr(parsed.code as *mut c_char) }; + let code_str = code_cstr.to_str().unwrap(); + assert_eq!(code_str, ""); + + let response_cstr = unsafe { CStr::from_ptr(parsed.response as *mut c_char) }; + let response_str = response_cstr.to_str().unwrap(); + assert_eq!(response_str, "Some random text"); + assert_eq!(parsed.length, "Some random text".len()); + } + + #[test] + fn test_parse_response_with_extra_whitespace() { + let input = "331 Password required \r\n"; + let parsed = parse_response_line(input).unwrap(); + + let code_cstr = unsafe { CStr::from_ptr(parsed.code as *mut c_char) }; + let code_str = code_cstr.to_str().unwrap(); + assert_eq!(code_str, "331"); + let response_cstr = unsafe { CStr::from_ptr(parsed.response as *mut c_char) }; + let response_str = response_cstr.to_str().unwrap(); + assert_eq!(response_str, " Password required"); + assert_eq!(parsed.length, " Password required".len()); + } + + #[test] + fn test_parse_response_with_trailing_newlines() { + let input = "220 Hello FTP Server\n"; + let parsed = parse_response_line(input).unwrap(); + + let code_cstr = unsafe { CStr::from_ptr(parsed.code as *mut c_char) }; + let code_str = code_cstr.to_str().unwrap(); + assert_eq!(code_str, "220"); + let response_cstr = unsafe { CStr::from_ptr(parsed.response as *mut c_char) }; + let response_str = response_cstr.to_str().unwrap(); + assert_eq!(response_str, "Hello FTP Server"); + assert_eq!(parsed.length, "Hello FTP Server".len()); + } + + #[test] + fn test_parse_empty_input() { + let input = ""; + assert!(parse_response_line(input).is_none()); + } + + #[test] + fn test_parse_only_newline() { + let input = "\n"; + assert!(parse_response_line(input).is_none()); + } + + #[test] + fn test_parse_malformed_code() { + let input = "99 Incorrect code\r\n"; + let parsed = parse_response_line(input).unwrap(); + + let code_cstr = unsafe { CStr::from_ptr(parsed.code as *mut c_char) }; + let code_str = code_cstr.to_str().unwrap(); + assert_eq!(code_str, ""); + let response_cstr = unsafe { CStr::from_ptr(parsed.response as *mut c_char) }; + let response_str = response_cstr.to_str().unwrap(); + assert_eq!(response_str, "99 Incorrect code"); + assert_eq!(parsed.length, "99 Incorrect code".len()); + } + + #[test] + fn test_parse_non_ascii_characters() { + let input = "500 '🌍 ABOR': unknown command\r\n"; + let parsed = parse_response_line(input).unwrap(); + + let code_cstr = unsafe { CStr::from_ptr(parsed.code as *mut c_char) }; + let code_str = code_cstr.to_str().unwrap(); + assert_eq!(code_str, "500"); + + let response_cstr = unsafe { CStr::from_ptr(parsed.response as *mut c_char) }; + let response_str = response_cstr.to_str().unwrap(); + assert_eq!(response_str, "'🌍 ABOR': unknown command"); + assert_eq!(parsed.length, "'🌍 ABOR': unknown command".len()); + } +} -- 2.47.2