* 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
+ code: *mut u8, // Response code as a string (may be null)
+ response: *mut u8, // Response string
+ length: usize, // Length of the response string
+ code_length: usize, // Length of the response code 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.
/// - (single response) "530 Login incorrect"
/// - (single response, no code) "Login incorrect"
fn parse_response_line(input: &str) -> Option<FTPResponseLine> {
- // 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();
+ // Split the input on the first \r\n to get the response line
+ let response_line = input.split("\r\n").next().unwrap_or("").trim_end();
if response_line.is_empty() {
- return None; // Ignore empty input
+ return None;
}
- // 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)
+ // Try to split off the 3-digit FTP status code
+ let (code_str, response_str) = match response_line.split_once(' ') {
+ Some((prefix, rest)) if prefix.len() == 3 && prefix.chars().all(|c| c.is_ascii_digit()) => {
+ (prefix, rest)
}
- _ => ("".to_string(), response_line), // No valid numeric code found
+ _ => ("", response_line),
};
- // Convert response and code to C strings
- let c_code = CString::new(code).ok()?;
- let c_response = CString::new(response).ok()?;
+ let code_bytes = code_str.as_bytes().to_vec();
+ let response_bytes = response_str.as_bytes().to_vec();
- // Compute memory usage
- let total_size = std::mem::size_of::<FTPResponseLine>()
- + c_code.as_bytes_with_nul().len()
- + c_response.as_bytes_with_nul().len();
+ let code_len = code_bytes.len();
+ let response_len = response_bytes.len();
+
+ let total_size = std::mem::size_of::<FTPResponseLine>() + code_len + response_len;
Some(FTPResponseLine {
- code: c_code.into_raw() as *mut u8,
- response: c_response.into_raw() as *mut u8,
- length: response.len(),
+ code: Box::into_raw(code_bytes.into_boxed_slice()) as *mut u8,
+ response: Box::into_raw(response_bytes.into_boxed_slice()) as *mut u8,
+ length: response_len,
+ code_length: code_len,
truncated: false,
total_size,
})
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 _ = Vec::from_raw_parts(
+ response.code,
+ response.code_length,
+ response.code_length,
+ );
}
if !response.response.is_null() {
- let _ = CString::from_raw(response.response as *mut c_char);
+ let _ = Box::from_raw(std::slice::from_raw_parts_mut(
+ response.response,
+ response.length,
+ ));
}
}
#[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();
+ let code_slice = unsafe { slice::from_raw_parts(parsed.code, parsed.code_length) };
+ let code_str = std::str::from_utf8(code_slice).expect("Invalid UTF-8");
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();
+ let response_slice = unsafe { slice::from_raw_parts(parsed.response, parsed.length) };
+ let response_str = std::str::from_utf8(response_slice).expect("Invalid UTF-8");
assert_eq!(response_str, "Welcome to FTP");
assert_eq!(parsed.length, "Welcome to FTP".len());
}
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();
+ let code_slice = unsafe { slice::from_raw_parts(parsed.code, parsed.code_length) };
+ let code_str = std::str::from_utf8(code_slice).expect("Invalid UTF-8");
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();
+ let response_slice = unsafe { slice::from_raw_parts(parsed.response, parsed.length) };
+ let response_str = std::str::from_utf8(response_slice).expect("Invalid UTF-8");
assert_eq!(response_str, "Some random text");
assert_eq!(parsed.length, "Some random text".len());
}
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();
+ let code_slice = unsafe { slice::from_raw_parts(parsed.code, parsed.code_length) };
+ let code_str = std::str::from_utf8(code_slice).expect("Invalid UTF-8");
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();
+ let response_slice = unsafe { slice::from_raw_parts(parsed.response, parsed.length) };
+ let response_str = std::str::from_utf8(response_slice).expect("Invalid UTF-8");
assert_eq!(response_str, " Password required");
assert_eq!(parsed.length, " Password required".len());
}
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();
+ let code_slice = unsafe { slice::from_raw_parts(parsed.code, parsed.code_length) };
+ let code_str = std::str::from_utf8(code_slice).expect("Invalid UTF-8");
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();
+ let response_slice = unsafe { slice::from_raw_parts(parsed.response, parsed.length) };
+ let response_str = std::str::from_utf8(response_slice).expect("Invalid UTF-8");
assert_eq!(response_str, "Hello FTP Server");
assert_eq!(parsed.length, "Hello FTP Server".len());
}
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();
+ let code_slice = unsafe { slice::from_raw_parts(parsed.code, parsed.code_length) };
+ let code_str = std::str::from_utf8(code_slice).expect("Invalid UTF-8");
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();
+ let response_slice = unsafe { slice::from_raw_parts(parsed.response, parsed.length) };
+ let response_str = std::str::from_utf8(response_slice).expect("Invalid UTF-8");
assert_eq!(response_str, "99 Incorrect code");
assert_eq!(parsed.length, "99 Incorrect code".len());
}
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();
+ let code_slice = unsafe { slice::from_raw_parts(parsed.code, parsed.code_length) };
+ let code_str = std::str::from_utf8(code_slice).expect("Invalid UTF-8");
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();
+ let response_slice = unsafe { slice::from_raw_parts(parsed.response, parsed.length) };
+ let response_str = std::str::from_utf8(response_slice).expect("Invalid UTF-8");
assert_eq!(response_str, "'🌍 ABOR': unknown command");
assert_eq!(parsed.length, "'🌍 ABOR': unknown command".len());
}
--- /dev/null
+/* 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.
+ */
+
+/**
+ *
+ * \author Jeff Lucovsky <jlucovsky@oisf.net>
+ *
+ * Implements the ftp.completion-code sticky buffer
+ *
+ */
+
+#include "suricata-common.h"
+
+#include "detect.h"
+#include "detect-parse.h"
+#include "detect-engine.h"
+#include "detect-engine-buffer.h"
+#include "detect-engine-helper.h"
+#include "detect-ftp-completion-code.h"
+
+#include "app-layer.h"
+#include "app-layer-ftp.h"
+
+#include "flow.h"
+
+#include "util-debug.h"
+
+#define KEYWORD_NAME "ftp.completion_code"
+#define KEYWORD_DOC "ftp-keywords.html#ftp-completion_code"
+#define BUFFER_NAME "ftp.completion_code"
+#define BUFFER_DESC "ftp completion code"
+
+static int g_ftp_ccode_buffer_id = 0;
+
+static int DetectFtpCompletionCodeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str)
+{
+ if (SCDetectBufferSetActiveList(de_ctx, s, g_ftp_ccode_buffer_id) < 0)
+ return -1;
+
+ if (DetectSignatureSetAppProto(s, ALPROTO_FTP) < 0)
+ return -1;
+
+ return 0;
+}
+
+static bool DetectFTPCompletionCodeGetData(DetectEngineThreadCtx *_det_ctx, const void *txv,
+ uint8_t _flow_flags, uint32_t index, const uint8_t **buffer, uint32_t *buffer_len)
+{
+ FTPTransaction *tx = (FTPTransaction *)txv;
+
+ if (tx->command_descriptor.command_code == FTP_COMMAND_UNKNOWN)
+ return false;
+
+ if (!TAILQ_EMPTY(&tx->response_list)) {
+ uint32_t count = 0;
+ FTPResponseWrapper *wrapper;
+ TAILQ_FOREACH (wrapper, &tx->response_list, next) {
+ DEBUG_VALIDATE_BUG_ON(wrapper->response == NULL);
+ if (index == count) {
+ *buffer = (const uint8_t *)wrapper->response->code;
+ *buffer_len = wrapper->response->code_length;
+ return true;
+ }
+ count++;
+ }
+ }
+
+ *buffer = NULL;
+ *buffer_len = 0;
+ return false;
+}
+
+void DetectFtpCompletionCodeRegister(void)
+{
+ /* ftp.completion_code sticky buffer */
+ sigmatch_table[DETECT_FTP_COMPLETION_CODE].name = KEYWORD_NAME;
+ sigmatch_table[DETECT_FTP_COMPLETION_CODE].desc =
+ "sticky buffer to match on the FTP completion code buffer";
+ sigmatch_table[DETECT_FTP_COMPLETION_CODE].url = "/rules/" KEYWORD_DOC;
+ sigmatch_table[DETECT_FTP_COMPLETION_CODE].Setup = DetectFtpCompletionCodeSetup;
+ sigmatch_table[DETECT_FTP_COMPLETION_CODE].flags |= SIGMATCH_NOOPT;
+
+ DetectAppLayerMultiRegister(
+ BUFFER_NAME, ALPROTO_FTP, SIG_FLAG_TOCLIENT, 0, DetectFTPCompletionCodeGetData, 2);
+
+ DetectBufferTypeSetDescriptionByName(BUFFER_NAME, BUFFER_DESC);
+
+ g_ftp_ccode_buffer_id = DetectBufferTypeGetByName(BUFFER_NAME);
+
+ SCLogDebug("registering " BUFFER_NAME " rule option");
+}