summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlijia <[email protected]>2024-11-07 09:52:08 +0800
committerlijia <[email protected]>2024-11-18 11:10:22 +0800
commita3d3efc5490c59684036a794b8f63f81b59685ea (patch)
treee4283d083e1172b814412f3af826f1cf1784068e
parentd0a868591470a4a9d71a65a5d540058e72c8d92c (diff)
ftp decoder rebase develop-2.0dev-ftp-v2.0
-rw-r--r--decoders/CMakeLists.txt3
-rw-r--r--decoders/ftp/CMakeLists.txt10
-rw-r--r--decoders/ftp/ftp_decoder_entry.c243
-rw-r--r--decoders/ftp/ftp_decoder_hash.c138
-rw-r--r--decoders/ftp/ftp_decoder_hash.h54
-rw-r--r--decoders/ftp/ftp_decoder_inner.h177
-rw-r--r--decoders/ftp/ftp_decoder_proto.c653
-rw-r--r--decoders/ftp/ftp_decoder_stat.c55
-rw-r--r--decoders/ftp/ftp_decoder_stat.h43
-rw-r--r--decoders/ftp/ftp_decoder_util.c400
-rw-r--r--decoders/ftp/ftp_decoder_util.h35
-rw-r--r--decoders/ftp/ftp_module.c192
-rw-r--r--decoders/ftp/version.map12
-rw-r--r--include/stellar/ftp.h88
-rw-r--r--infra/version.map7
-rw-r--r--test/CMakeLists.txt5
-rw-r--r--test/decoders/ftp/CMakeLists.txt64
-rw-r--r--test/decoders/ftp/ftp_gtest_main.cpp97
-rw-r--r--test/decoders/ftp/ftp_gtest_module.cpp250
-rw-r--r--test/decoders/ftp/ftp_module.toml13
-rw-r--r--test/decoders/ftp/gtest_ftp_decoder.cpp236
-rw-r--r--test/decoders/ftp/json/01-ftp-port-upload-download.json147
-rw-r--r--test/decoders/ftp/json/01-ftp-port-upload-download_C2S.json78
-rw-r--r--test/decoders/ftp/json/01-ftp-port-upload-download_S2C.json28
-rw-r--r--test/decoders/ftp/json/02-ftp_v6_1.json160
-rw-r--r--test/decoders/ftp/json/03-ipv6_eport_upload_download.json167
-rw-r--r--test/decoders/ftp/json/04-ftp-banner-no-ftp-characters.json54
-rw-r--r--test/decoders/ftp/json/05-only-ctrl-link.json33
-rw-r--r--test/decoders/ftp/json/06-ftp_pasv-upload-download.json135
-rw-r--r--test/decoders/ftp/json/06-ftp_pasv-upload-download_C2S.json54
-rw-r--r--test/decoders/ftp/json/06-ftp_pasv-upload-download_S2C.json46
-rw-r--r--test/decoders/ftp/json/07-ftp-start-with-syst-command.json37
-rw-r--r--test/decoders/ftp/json/08-ftp_EPSV_229_ipv4.json34
-rw-r--r--test/decoders/ftp/json/10-ftp-sdedu.json101
-rw-r--r--test/decoders/ftp/json/11-ftp-sjtu.json354
-rw-r--r--test/decoders/ftp/json/empty.json1
-rw-r--r--test/decoders/ftp/pcap/01-ftp-port-upload-download.pcapbin0 -> 1745959 bytes
-rw-r--r--test/decoders/ftp/pcap/01-ftp-port-upload-download_C2S.pcapbin0 -> 722094 bytes
-rw-r--r--test/decoders/ftp/pcap/01-ftp-port-upload-download_S2C.pcapbin0 -> 1023889 bytes
-rw-r--r--test/decoders/ftp/pcap/02-ftp_v6_1.pcapbin0 -> 12915 bytes
-rw-r--r--test/decoders/ftp/pcap/03-ipv6_eport_upload_download.pcapbin0 -> 12210 bytes
-rw-r--r--test/decoders/ftp/pcap/04-ftp-banner-no-ftp-characters.pcapbin0 -> 5424 bytes
-rw-r--r--test/decoders/ftp/pcap/05-only-ctrl-link.pcapbin0 -> 3083 bytes
-rw-r--r--test/decoders/ftp/pcap/06-ftp_pasv-upload-download.pcapbin0 -> 244252 bytes
-rw-r--r--test/decoders/ftp/pcap/06-ftp_pasv-upload-download_C2S.pcapbin0 -> 3991 bytes
-rw-r--r--test/decoders/ftp/pcap/06-ftp_pasv-upload-download_S2C.pcapbin0 -> 4036 bytes
-rw-r--r--test/decoders/ftp/pcap/07-ftp-start-with-syst-command.pcapbin0 -> 4347 bytes
-rw-r--r--test/decoders/ftp/pcap/08-ftp_EPSV_229_ipv4.pcapbin0 -> 3679 bytes
-rw-r--r--test/decoders/ftp/pcap/10-ftp-sdedu.pcapbin0 -> 45153 bytes
-rw-r--r--test/decoders/ftp/pcap/11-ftp-sjtu.pcapbin0 -> 6310804 bytes
50 files changed, 4201 insertions, 3 deletions
diff --git a/decoders/CMakeLists.txt b/decoders/CMakeLists.txt
index 7946822..ef8778c 100644
--- a/decoders/CMakeLists.txt
+++ b/decoders/CMakeLists.txt
@@ -2,4 +2,5 @@ add_subdirectory(lpi_plus)
#add_subdirectory(http)
#add_subdirectory(socks)
#add_subdirectory(stratum)
-#add_subdirectory(session_flags) \ No newline at end of file
+#add_subdirectory(session_flags)
+add_subdirectory(ftp) \ No newline at end of file
diff --git a/decoders/ftp/CMakeLists.txt b/decoders/ftp/CMakeLists.txt
new file mode 100644
index 0000000..578ee6a
--- /dev/null
+++ b/decoders/ftp/CMakeLists.txt
@@ -0,0 +1,10 @@
+add_definitions(-fPIC)
+include_directories(${CMAKE_SOURCE_DIR}/deps)
+
+set(FTP_SRC ftp_module.c ftp_decoder_entry.c ftp_decoder_hash.c ftp_decoder_proto.c ftp_decoder_stat.c ftp_decoder_util.c )
+
+add_library(ftp ${FTP_SRC})
+set_target_properties(ftp PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version.map")
+target_include_directories(ftp PUBLIC ${CMAKE_SOURCE_DIR}/deps/)
+target_link_libraries(ftp fieldstat4 toml)
+set_target_properties(ftp PROPERTIES PREFIX "")
diff --git a/decoders/ftp/ftp_decoder_entry.c b/decoders/ftp/ftp_decoder_entry.c
new file mode 100644
index 0000000..dfceadc
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_entry.c
@@ -0,0 +1,243 @@
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "ftp_decoder_inner.h"
+#include "stellar/ftp.h"
+#include "ftp_decoder_util.h"
+#include "stellar/log.h"
+#include <stellar/session.h>
+#include <sys/types.h>
+
+ void ftp_msg_free_cb(void *msg, void *msg_free_arg UNUSED)
+ {
+ struct ftp_message *ftp_msg = (struct ftp_message *)msg;
+ struct ftp_decoder_exdata *ftp_ext = ftp_msg->ftp_ext_ref;
+ if (ftp_msg->topic_type == FTP_TOPIC_CTRL_DTP && ftp_ext->link_type == FTP_LINK_CTRL)
+ {
+ /* multiple dtp messages in ctrl link */
+ ftp_exdata_dtp_free(ftp_msg->dtp_ref);
+ }
+ free(msg);
+ }
+
+ static inline int ftp_is_session_closed(enum session_state state, const char *tcp_payload)
+ {
+ if (unlikely((NULL == tcp_payload) &&
+ (SESSION_STATE_CLOSING == state || SESSION_STATE_CLOSED == state)))
+ {
+ return 1;
+ }
+ return 0;
+ }
+
+ static struct ftp_decoder_exdata *ftp_exdata_new(struct session *sess, struct ftp_decoder *ftp_env,
+ const char *tcp_payload, uint32_t tcp_payload_len, enum flow_type flow_dir)
+ {
+ uint64_t tcp_seg_num = session_get_stat(sess, flow_dir, STAT_TCP_SEGMENTS_RECEIVED);
+ if (tcp_seg_num > 1)
+ {
+ return NULL;
+ }
+
+ int is_ctrl_link = ftp_ctrl_identify(sess, tcp_payload, tcp_payload_len, flow_dir);
+ if (is_ctrl_link)
+ {
+ struct ftp_decoder_exdata *ctrl_ext = (struct ftp_decoder_exdata *)calloc(1, sizeof(struct ftp_decoder_exdata));
+ ctrl_ext->sess_ref = sess;
+ ctrl_ext->ftp_env_ref = ftp_env;
+ ctrl_ext->link_type = FTP_LINK_CTRL;
+ ctrl_ext->reference = 1;
+ snprintf(ctrl_ext->ctrl_ext.current_working_dir, sizeof(ctrl_ext->ctrl_ext.current_working_dir), "/");
+ int thread_idx = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_CTRL_LINK_NEW, 1);
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp ctrl link new: %s", session_get_readable_addr(sess));
+ session_set_exdata(sess, ftp_env->exdata_id, ctrl_ext);
+ return ctrl_ext;
+ }
+
+ struct ftp_decoder_exdata *data_ext = ftp_data_identify(sess, ftp_env);
+ if (data_ext)
+ {
+ data_ext->link_type = FTP_LINK_DATA;
+ data_ext->sess_ref = sess;
+ data_ext->ftp_env_ref = ftp_env;
+ data_ext->reference++;
+ int thread_idx = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_DATA_LINK_NEW, 1);
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp data link new, session: %s", session_get_readable_addr(sess));
+ session_set_exdata(sess, ftp_env->exdata_id, data_ext);
+ return data_ext;
+ }
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "not ftp session: %s", session_get_readable_addr(sess));
+ return NULL;
+ }
+
+ void ftp_decoder_do_exdata_free(struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env UNUSED)
+ {
+ if (NULL == ftp_ext)
+ {
+ return;
+ }
+ ftp_ext->reference--;
+ if (ftp_ext->reference > 0)
+ {
+ return;
+ }
+ ftp_exdata_dtp_free(ftp_ext->dtp);
+ ftp_login_info_free(&ftp_ext->ftp_login_pri);
+ free(ftp_ext);
+ }
+
+ void ftp_decoder_exdata_free_cb(int idx UNUSED, void *ex_ptr, void *arg)
+ {
+ if (NULL == ex_ptr)
+ {
+ return;
+ }
+ struct ftp_decoder *ftp_env = (struct ftp_decoder *)arg;
+ struct ftp_decoder_exdata *ftp_ext = (struct ftp_decoder_exdata *)ex_ptr;
+ int thread_idx = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ if (ftp_ext->link_type == FTP_LINK_CTRL)
+ {
+ ftp_decoder_stat_incrby(thread_idx, (struct ftp_decoder *)ftp_env, FTPD_STAT_CTRL_LINK_FREE, 1);
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp ctrl link free: %s", session_get_readable_addr(ftp_ext->sess_ref));
+ }
+ else
+ {
+ ftp_decoder_stat_incrby(thread_idx, (struct ftp_decoder *)ftp_env, FTPD_STAT_DATA_LINK_FREE, 1);
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp data link free: %s", session_get_readable_addr(ftp_ext->sess_ref));
+ /* data link exdata freed in ftp_del_hash_item()
+ * maybe:
+ * 1. data link is not established success, ftp_del_hash_item() will free exdata
+ * 2. data link is established success, but ctrl link stor/retr command is not completed, so need keep the exdata in hash item, dtp information could be updated later
+ */
+ }
+ ftp_decoder_do_exdata_free(ftp_ext, ftp_env);
+ }
+
+ static void ftp_ctrl_link_entry(int thread_idx, struct session *sess, struct ftp_decoder_exdata *ftp_ext,
+ struct ftp_decoder *ftp_env, const char *tcp_payload, uint32_t tcp_payload_len, enum flow_type flow_dir)
+ {
+ if (unlikely(NULL == tcp_payload))
+ {
+ return;
+ }
+ if (ftp_cmd_readline(&ftp_ext->ctrl_ext.cmd_line, tcp_payload, tcp_payload_len) < 0)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp session:%s, command line parse error! %.*s",
+ session_get_readable_addr(sess), (int)tcp_payload_len >= 32 ? 32 : tcp_payload_len, tcp_payload);
+ ftp_ext->ignore_session = 1;
+ return;
+ }
+
+ int thread_id = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_id, ftp_env, FTPD_STAT_CTRL_CMD, 1);
+ if (FLOW_TYPE_C2S == flow_dir)
+ {
+ if (ftp_command_process(sess, ftp_ext, ftp_env) < 0)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp session:%s,command process error! %.*s",
+ session_get_readable_addr(sess), (int)tcp_payload_len >= 32 ? 32 : tcp_payload_len, tcp_payload);
+ ftp_ext->ignore_session = 1;
+ return;
+ }
+ ftp_decoder_push_msg(FTP_TOPIC_CTRL_REQ_LINE, sess, ftp_ext, ftp_env);
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_CTRL_LINK_BYTES_C2S, (long long)tcp_payload_len);
+ }
+ else
+ {
+ if (ftp_reply_process(sess, ftp_ext, ftp_env) < 0)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp session:%s, reply process error! %.*s",
+ session_get_readable_addr(sess), (int)tcp_payload_len >= 32 ? 32 : tcp_payload_len, tcp_payload);
+ ftp_ext->ignore_session = 1;
+ return;
+ }
+ ftp_decoder_push_msg(FTP_TOPIC_CTRL_RES_LINE, sess, ftp_ext, ftp_env);
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_CTRL_LINK_BYTES_S2C, (long long)tcp_payload_len);
+ }
+ if (ftp_ext->dtp != NULL)
+ {
+ ftp_decoder_push_msg(FTP_TOPIC_CTRL_DTP, sess, ftp_ext, ftp_env);
+ ftp_ext->dtp = NULL; // ownership move to message after push, update in next command
+ }
+ return;
+ }
+
+ static void ftp_data_link_entry(int thread_idx, struct session *sess, struct ftp_decoder_exdata *ftp_ext,
+ struct ftp_decoder *ftp_env, const char *tcp_payload UNUSED, uint32_t tcp_payload_len, enum flow_type flow_dir)
+ {
+ ftp_ext->data_ext.chunk = tcp_payload;
+ ftp_ext->data_ext.chunk_size = tcp_payload_len;
+ ftp_ext->data_ext.offset += tcp_payload_len;
+ if (ftp_is_session_closed(session_get_current_state(sess), tcp_payload))
+ {
+ ftp_ext->data_ext.is_finished = 1;
+ ftp_decoder_push_msg(FTP_TOPIC_DATA_DTP, sess, ftp_ext, ftp_env);
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp data link closed, %s", session_get_readable_addr(sess));
+ return;
+ }
+
+ ftp_decoder_push_msg(FTP_TOPIC_DATA_DTP, sess, ftp_ext, ftp_env);
+ if (flow_dir == FLOW_TYPE_C2S)
+ {
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_DATA_LINK_BYTES_C2S, (long long)tcp_payload_len);
+ }
+ else
+ {
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_DATA_LINK_BYTES_S2C, (long long)tcp_payload_len);
+ }
+ }
+
+ void ftp_on_tcp_stream_cb(struct session *sess, enum session_state state UNUSED, const char *tcp_payload, uint32_t tcp_payload_len, void *args)
+ {
+ enum flow_type flow_dir = session_get_flow_type(sess);
+ struct ftp_decoder *ftp_env = (struct ftp_decoder *)args;
+ struct ftp_decoder_exdata *ftp_ext = (struct ftp_decoder_exdata *)session_get_exdata(sess, ftp_env->exdata_id);
+ if (NULL == ftp_ext)
+ {
+ if (FLOW_TYPE_C2S != flow_dir && FLOW_TYPE_S2C != flow_dir) // maybe timeout in polling event
+ {
+ return;
+ }
+ ftp_ext = ftp_exdata_new(sess, ftp_env, tcp_payload, tcp_payload_len, flow_dir);
+ }
+ if (NULL == ftp_ext || ftp_ext->ignore_session) // not ftp session
+ {
+ return;
+ }
+ int thread_idx = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ if (FTP_LINK_CTRL == ftp_ext->link_type)
+ {
+ ftp_ctrl_link_entry(thread_idx, sess, ftp_ext, ftp_env, tcp_payload, tcp_payload_len, flow_dir);
+ }
+ else
+ {
+ ftp_data_link_entry(thread_idx, sess, ftp_ext, ftp_env, tcp_payload, tcp_payload_len, flow_dir);
+ }
+ }
+
+ int ftp_decoder_push_msg(enum ftp_topic_type topic_type, struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ struct ftp_message *fmsg = (struct ftp_message *)calloc(1, sizeof(struct ftp_message));
+ fmsg->sess_ref = sess;
+ fmsg->topic_type = topic_type;
+ fmsg->ftp_ext_ref = ftp_ext;
+ fmsg->ftp_env_ref = ftp_env;
+ fmsg->dtp_ref = ftp_ext->dtp;
+
+ struct mq_runtime *mq_rt = module_manager_get_mq_runtime(ftp_env->mod_mgr_ref);
+ int ret = mq_runtime_publish_message(mq_rt, ftp_env->ftp_topic_mgr->topic_compose[topic_type].topic_id, fmsg);
+ if (ret < 0)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp publist message failed, topic:%s, session:%s",
+ ftp_env->ftp_topic_mgr->topic_compose[topic_type].topic_name, session_get_readable_addr(sess));
+ free(fmsg);
+ }
+ return ret;
+ }
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/decoders/ftp/ftp_decoder_hash.c b/decoders/ftp/ftp_decoder_hash.c
new file mode 100644
index 0000000..3a03368
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_hash.c
@@ -0,0 +1,138 @@
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_hash.h"
+#include "ftp_decoder_util.h"
+#include <arpa/inet.h>
+#include <netinet/tcp.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include "uthash/uthash.h"
+#include "uthash/utlist.h"
+
+ static __thread struct ftp_datalink_htable *__thread_local_ftp_datalink_htable = NULL;
+ static __thread struct ftp_datalink_htable *__thread_local_ftp_datalink_htable_fifo_head = NULL;
+ static __thread char __ftp_hash_string_buf[FTP_HASH_STRING_BUF_SIZE];
+ static __thread int ftp_local_thread_idx;
+ static __thread struct ftp_decoder *ftp_local_env;
+
+ static void ftp_del_hash_item(struct ftp_datalink_htable *item)
+ {
+ ftp_decoder_stat_incrby(ftp_local_thread_idx, ftp_local_env, FTPD_STAT_DATA_LINK_HTABLE_ITEMS, -1);
+ HASH_DEL(__thread_local_ftp_datalink_htable, item);
+ DL_DELETE(__thread_local_ftp_datalink_htable_fifo_head, item);
+ ftp_decoder_do_exdata_free(item->ftp_ext, ftp_local_env);
+ free(item); // only free hash item, but not free ftp_ext
+ }
+
+ static void ftp_hash_cleanup_timeout(void)
+ {
+ struct ftp_datalink_htable *to_del, *tmp;
+ time_t now = time(NULL);
+ DL_FOREACH_SAFE(__thread_local_ftp_datalink_htable_fifo_head, to_del, tmp)
+ {
+ if (now - to_del->insert_htable_time > FTP_HASH_ITEM_TIMEOUT)
+ {
+ ftp_del_hash_item(to_del);
+ }
+ }
+ }
+
+ int ftp_hash_add(const ftp_hash_key_t *key, u_int32_t key_len, struct ftp_datalink_htable *new_item)
+ {
+ ftp_hash_cleanup_timeout();
+ struct ftp_datalink_htable *in_hash_item = NULL;
+ HASH_FIND(hh, __thread_local_ftp_datalink_htable, key, key_len, in_hash_item);
+ if (in_hash_item != NULL)
+ {
+ return -1; // duplicate
+ }
+ HASH_ADD(hh, __thread_local_ftp_datalink_htable, hkey, key_len, new_item);
+ DL_APPEND(__thread_local_ftp_datalink_htable_fifo_head, new_item);
+ return 0;
+ }
+
+ void ftp_hash_del(const ftp_hash_key_t *key)
+ {
+ struct ftp_datalink_htable *tmp = NULL;
+ HASH_FIND(hh, __thread_local_ftp_datalink_htable, key, sizeof(ftp_hash_key_t), tmp);
+ if (tmp)
+ {
+ ftp_del_hash_item(tmp);
+ }
+ }
+
+ void ftp_make_hkey_v4(ftp_hash_key_t *keyv4, uint32_t sip_net, uint32_t dip_net, uint16_t dip_port_net)
+ {
+ memset(keyv4, 0, sizeof(ftp_hash_key_t));
+ keyv4->af_inet = AF_INET;
+ keyv4->saddr4 = sip_net;
+ keyv4->daddr4 = dip_net;
+ keyv4->sport = 0;
+ keyv4->dport = dip_port_net;
+ }
+
+ void ftp_make_hkey_v6(ftp_hash_key_t *keyv6, const struct in6_addr *sip, const struct in6_addr *dip, uint16_t dip_port_net)
+ {
+ memset(keyv6, 0, sizeof(ftp_hash_key_t));
+ keyv6->af_inet = AF_INET6;
+ memcpy(&keyv6->saddr6, sip, sizeof(struct in6_addr));
+ memcpy(&keyv6->daddr6, dip, sizeof(struct in6_addr));
+ keyv6->sport = 0;
+ keyv6->dport = dip_port_net;
+ }
+
+ struct ftp_datalink_htable *ftp_hash_search(const ftp_hash_key_t *key)
+ {
+ ftp_hash_cleanup_timeout();
+ struct ftp_datalink_htable *tmp = NULL;
+ HASH_FIND(hh, __thread_local_ftp_datalink_htable, key, sizeof(ftp_hash_key_t), tmp);
+ return tmp;
+ }
+
+ const char *ftp_hash_key_to_str(const ftp_hash_key_t *hkey)
+ {
+ char sip_str[INET6_ADDRSTRLEN], dip_str[INET6_ADDRSTRLEN];
+ unsigned short sport_host, dport_host;
+ if (AF_INET == hkey->af_inet)
+ {
+ inet_ntop(AF_INET, &hkey->saddr4, sip_str, INET_ADDRSTRLEN);
+ inet_ntop(AF_INET, &hkey->daddr4, dip_str, INET_ADDRSTRLEN);
+ sport_host = ntohs(hkey->sport);
+ dport_host = ntohs(hkey->dport);
+ }
+ else
+ {
+ inet_ntop(AF_INET6, &hkey->saddr6, sip_str, INET6_ADDRSTRLEN);
+ inet_ntop(AF_INET6, &hkey->daddr6, dip_str, INET6_ADDRSTRLEN);
+ sport_host = ntohs(hkey->sport);
+ dport_host = ntohs(hkey->dport);
+ }
+ snprintf(__ftp_hash_string_buf, sizeof(__ftp_hash_string_buf), "%s:%u -> %s:%u", sip_str, sport_host, dip_str, dport_host);
+ return __ftp_hash_string_buf;
+ }
+
+ struct module *ftp_on_thread_init(UNUSED struct module_manager *mod_mgr, UNUSED int thread_id, struct module *mod)
+ {
+ ftp_local_thread_idx = thread_id;
+ __thread_local_ftp_datalink_htable = NULL;
+ ftp_local_env = (struct ftp_decoder *)module_get_ctx(mod);
+ return mod;
+ }
+
+ void ftp_on_thread_exit(UNUSED struct module_manager *mod_mgr, UNUSED int thread_id, UNUSED struct module *mod)
+ {
+ struct ftp_datalink_htable *item, *tmp;
+ HASH_ITER(hh, __thread_local_ftp_datalink_htable, item, tmp)
+ {
+ ftp_del_hash_item(item);
+ }
+ HASH_CLEAR(hh, __thread_local_ftp_datalink_htable);
+ }
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/decoders/ftp/ftp_decoder_hash.h b/decoders/ftp/ftp_decoder_hash.h
new file mode 100644
index 0000000..f8f5468
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_hash.h
@@ -0,0 +1,54 @@
+#pragma once
+#include <stddef.h>
+#include <arpa/inet.h>
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "stellar/session.h"
+#include "stellar/log.h"
+#include "stellar/module.h"
+#include "stellar/mq.h"
+#include "uthash/uthash.h"
+#ifdef __cplusplus
+}
+#endif
+
+#define FTP_HASH_STRING_BUF_SIZE (256)
+#define FTP_HASH_ITEM_TIMEOUT (30) // seconds
+
+struct ftp_session_addr /* network order */
+{
+ int af_inet; // AF_INET or AF_INET6
+ uint16_t sport;
+ uint16_t dport;
+ union
+ {
+ uint32_t saddr4;
+ struct in6_addr saddr6;
+ };
+ union
+ {
+ uint32_t daddr4;
+ struct in6_addr daddr6;
+ };
+} __attribute__((packed));
+typedef struct ftp_session_addr ftp_hash_key_t;
+
+struct ftp_datalink_htable
+{
+ UT_hash_handle hh;
+ struct ftp_datalink_htable *next, *prev; // for timeout fifo list
+ ftp_hash_key_t hkey;
+ time_t insert_htable_time;
+ struct ftp_decoder_exdata *ftp_ext; // data link exdata
+};
+
+int ftp_hash_add(const ftp_hash_key_t *key, u_int32_t key_len, struct ftp_datalink_htable *new_item);
+struct ftp_datalink_htable *ftp_hash_search(const ftp_hash_key_t *key);
+void ftp_hash_del(const ftp_hash_key_t *key);
+void ftp_session_get_addr(const struct session *sess, struct ftp_session_addr *addr);
+void ftp_make_hkey_v4(ftp_hash_key_t *keyv4, uint32_t sip_net, uint32_t dip_net, uint16_t dip_port_net);
+void ftp_make_hkey_v6(ftp_hash_key_t *keyv6, const struct in6_addr *sip, const struct in6_addr *dip, uint16_t dip_port_net);
+const char *ftp_hash_key_to_str(const struct ftp_session_addr *hkey);
+void ftp_session_get_addr(const struct session *sess, struct ftp_session_addr *addr); \ No newline at end of file
diff --git a/decoders/ftp/ftp_decoder_inner.h b/decoders/ftp/ftp_decoder_inner.h
new file mode 100644
index 0000000..4ccaf46
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_inner.h
@@ -0,0 +1,177 @@
+#pragma once
+#include <linux/limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "stellar/ftp.h"
+#include "stellar/log.h"
+#include "stellar/module.h"
+#include "stellar/mq.h"
+#include "stellar/session.h"
+#include "stellar/utils.h"
+
+#ifdef __cplusplus
+}
+#endif
+#include "ftp_decoder_stat.h"
+#include "ftp_decoder_hash.h"
+
+#define FTP_IDENTIRY_MIN_LEN 4
+#define FTP_IDENTIRY_MAX_LEN 32
+#define FTP_CMD_MAX_LENGTH 256
+#define FTP_URL_MAX_LEN 2048
+
+#define FTP_DECODER_FIELDSTAT_NAME "ftp_decoder"
+#define FTP_DECODER_DXDATA_NAME "ftp_decoder_exdata"
+
+#define FTP_DECODER_FIELDSTAT_OUTPUT_FILE "./log/ftp_decoder.fs4"
+#define FTP_DECODER_FIELDSTAT_OUTPUT_INTERVAL 1
+
+#ifndef UNUSED
+#define UNUSED __attribute__((unused))
+#endif
+
+#define IOVEC_PRINT(iov) (int)(iov).iov_len, (char *)(iov).iov_base
+#define IOVEC_PRINT_PTR(iov_p) (int)(iov_p->iov_len), (char *)(iov_p->iov_base)
+#ifndef fstring
+typedef struct iovec fstring;
+#endif
+enum ftp_link_type
+{
+ FTP_LINK_CTRL,
+ FTP_LINK_DATA,
+};
+
+enum ftp_data_link_type
+{
+ FTP_DATA_LINK_FILE,
+ FTP_DATA_LINK_INVENTORY,
+};
+
+struct ftp_interact_line
+{
+ fstring cmd_line; // full line but no "\r\n", pointer to packet payload
+ fstring cmd_refer; // pointer to packet payload first word
+ fstring arg_refer; // pointer to packet payload after first word
+};
+
+struct ftp_login_internal
+{
+ fstring username; // iov_base is C string with '\0'
+ fstring password; // iov_base is C string with '\0'
+};
+
+struct ftp_decoder_ctrl_exdata
+{
+ char current_working_dir[PATH_MAX]; // default is "/"
+ enum ftp_command cmd_type;
+ enum ftp_reply_code reply_code;
+ struct ftp_interact_line cmd_line; // per tcp segment
+ ftp_hash_key_t last_data_link_key; // by cmd port or pasv
+ enum ftp_transfer_mode mode;
+ enum ftp_transfer_dir dir;
+};
+
+struct ftp_decoder_data_exdata
+{
+ const char *chunk; // refer to tcp payload
+ size_t chunk_size;
+ size_t offset;
+ int is_finished;
+};
+
+struct ftp_decoder_exdata // per session
+{
+ struct session *sess_ref;
+ struct ftp_decoder *ftp_env_ref;
+ enum ftp_link_type link_type;
+ int reference;
+ int ignore_session;
+ struct ftp_login_internal ftp_login_pri; // per session
+ struct ftp_dtp *dtp; // per transaction. data link dtp is deep copy from ctrl link, need be free when session close
+ struct ftp_decoder_ctrl_exdata ctrl_ext;
+ struct ftp_decoder_data_exdata data_ext;
+};
+
+struct ftp_interact_cmd_parser
+{
+ enum ftp_command cmd_type;
+ const char *cmd_name;
+ size_t cmd_len;
+ int (*cmd_handler)(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env);
+};
+
+struct ftp_interact_reply_parser
+{
+ enum ftp_reply_code reply_code_type;
+ const char *reply_code;
+ size_t reply_len;
+ int (*reply_handler)(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env);
+};
+
+enum ftp_topic_type
+{
+ FTP_TOPIC_CTRL_REQ_LINE = 0,
+ FTP_TOPIC_CTRL_RES_LINE,
+ FTP_TOPIC_CTRL_DTP,
+ FTP_TOPIC_DATA_DTP,
+ FTP_TOPIC_MAX,
+};
+
+struct ftp_topic_compose
+{
+ enum ftp_topic_type topic_type;
+ int topic_id;
+ const char *topic_name;
+};
+
+struct ftp_topic_manager
+{
+ struct ftp_topic_compose topic_compose[FTP_TOPIC_MAX];
+};
+
+struct ftp_decoder
+{
+ struct module_manager *mod_mgr_ref;
+ struct logger *logger_ref;
+ struct ftp_topic_manager *ftp_topic_mgr;
+ int exdata_id;
+ struct ftp_decoder_stat stat;
+};
+
+struct ftp_message
+{
+ struct session *sess_ref;
+ enum ftp_topic_type topic_type;
+ struct ftp_decoder_exdata *ftp_ext_ref;
+ struct ftp_decoder *ftp_env_ref;
+ struct ftp_dtp *dtp_ref;
+};
+
+void ftp_msg_free_cb(void *msg, void *msg_free_arg);
+void ftp_decoder_exdata_free_cb(int idx, void *ex_ptr, void *arg);
+void ftp_decoder_do_exdata_free(struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env);
+void ftp_on_tcp_stream_cb(struct session *sess, enum session_state state, const char *tcp_payload, uint32_t tcp_payload_len, void *args);
+int ftp_command_process(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env);
+int ftp_reply_process(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env);
+
+void ftp_exdata_dtp_free(struct ftp_dtp *dtp);
+void ftp_login_info_free(struct ftp_login_internal *login_info);
+int ftp_hash_table_create(struct ftp_decoder *fenv);
+void ftp_hash_table_destroy(struct ftp_decoder *fenv);
+int ftp_ctrl_identify_by_payload(const char *payload, size_t len, enum flow_type curdir);
+int ftp_ctrl_identify_by_addr(struct session *sess);
+int ftp_ctrl_identify(struct session *sess, const char *payload, size_t len, enum flow_type curdir);
+struct ftp_decoder_exdata *ftp_data_identify(struct session *sess, struct ftp_decoder *fenv);
+int ftp_cmd_readline(struct ftp_interact_line *line, const char *payload, size_t len);
+void ftp_ctrl_entry(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *fenv);
+void ftp_data_entry(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *fenv);
+int ftp_decoder_push_msg(enum ftp_topic_type topic_type, struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *fenv);
+int ftp_parse_ipv4_port_style(const fstring *cmd_str, unsigned int *ipv4_net, unsigned short *port_net);
+int ftp_parse_ipv6_port_style(const fstring *cmd_str, unsigned short *port_net);
+int ftp_parse_eprt_ipport_style(const fstring *arg_str, struct in6_addr *ipd_addr, unsigned short *port_net, struct ftp_decoder *ftp_env); \ No newline at end of file
diff --git a/decoders/ftp/ftp_decoder_proto.c b/decoders/ftp/ftp_decoder_proto.c
new file mode 100644
index 0000000..064d176
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_proto.c
@@ -0,0 +1,653 @@
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+#include "ftp_decoder_hash.h"
+#include <sys/types.h>
+#include <arpa/inet.h>
+#include <time.h>
+ /*
+ https://datatracker.ietf.org/doc/html/rfc959
+ -------------
+ |/---------\|
+ || User || --------
+ ||Interface|<--->| User |
+ |\----^----/| --------
+ ---------- | | |
+ |/------\| FTP Commands |/----V----\|
+ ||Server|<---------------->| User ||
+ || PI || FTP Replies || PI ||
+ |\--^---/| |\----^----/|
+ | | | | | |
+ -------- |/--V---\| Data |/----V----\| --------
+ | File |<--->|Server|<---------------->| User |<--->| File |
+ |System| || DTP || Connection || DTP || |System|
+ -------- |\------/| |\---------/| --------
+ ---------- -------------
+
+ Server-FTP USER-FTP
+ */
+
+ /*
+ ctrl link command data link tcp/ip stack event
+ ---------------------------------------------------------------------------------------------------
+ PORT 192,168,38,2,202,95
+ listen tcp port: 51807
+ 200 PORT command successful.
+ LIST
+ server use src tcp port 20 connecting to client tcp port 51807
+ data connection is established
+ send 'ls -l' result to cliet
+ 226 Directory send OK.
+ close the tcp data connection
+ waiting for next command...
+ */
+
+ static int ftp_create_new_datalink_hash_item(struct ftp_decoder *ftp_env, struct ftp_decoder_exdata *ftp_ext)
+ {
+ int thread_id = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_id, ftp_env, FTPD_STAT_DATA_LINK_HTABLE_ITEMS, 1);
+
+ struct ftp_datalink_htable *hitem = (struct ftp_datalink_htable *)calloc(1, sizeof(struct ftp_datalink_htable));
+ hitem->ftp_ext = ftp_exdata_deep_clone(ftp_ext);
+ hitem->ftp_ext->link_type = FTP_LINK_DATA;
+ hitem->insert_htable_time = time(NULL);
+ memcpy(&hitem->hkey, &ftp_ext->ctrl_ext.last_data_link_key, sizeof(ftp_hash_key_t));
+ memset(&ftp_ext->data_ext, 0, sizeof(struct ftp_decoder_data_exdata));
+ int ret = ftp_hash_add(&ftp_ext->ctrl_ext.last_data_link_key, sizeof(ftp_hash_key_t), hitem);
+ if (ret < 0)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp data link hash item add failed, session:%s, hashkey: %s",
+ session_get_readable_addr(ftp_ext->sess_ref), ftp_hash_key_to_str(&ftp_ext->ctrl_ext.last_data_link_key));
+ ftp_decoder_do_exdata_free(hitem->ftp_ext, ftp_env);
+ free(hitem);
+ return -1;
+ }
+ return 0;
+ }
+
+ /* According to the CWD, server address, file name of the current command, concatenated into a complete URL */
+ static void ftp_assemble_uri(struct session *sess, struct ftp_decoder_exdata *ftp_ext)
+ {
+ char dip_str[INET6_ADDRSTRLEN] = {};
+ int url_len = 0;
+ struct ftp_session_addr ses_addr = {};
+ ftp_session_get_addr(sess, &ses_addr);
+ uint16_t server_port_host = ntohs(ses_addr.dport);
+ if (ses_addr.af_inet == AF_INET)
+ {
+ inet_ntop(AF_INET, &ses_addr.daddr4, dip_str, INET_ADDRSTRLEN);
+ }
+ else
+ {
+ inet_ntop(AF_INET6, &ses_addr.daddr6, dip_str, INET6_ADDRSTRLEN);
+ }
+
+ char file_name[PATH_MAX] = {};
+ char absolute_file_path[PATH_MAX] = {};
+ char tmp_url[PATH_MAX] = {};
+ snprintf(file_name, sizeof(file_name), "%.*s", IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ ftp_join_absolute_path(ftp_ext->ctrl_ext.current_working_dir, (const char *)file_name, absolute_file_path, PATH_MAX);
+ if (server_port_host != 21)
+ {
+ url_len = snprintf(tmp_url, sizeof(tmp_url), "ftp://%s:%u%s", dip_str, server_port_host, absolute_file_path);
+ }
+ else
+ {
+ url_len = snprintf(tmp_url, sizeof(tmp_url), "ftp://%s%s", dip_str, absolute_file_path);
+ }
+
+ struct ftp_dtp *dtp = ftp_ext->dtp;
+ if (dtp->uri != NULL)
+ {
+ free((void *)dtp->uri); // update every time for retr/stor commands
+ }
+ dtp->uri = (char *)calloc(1, url_len + 1);
+ memcpy((char *)dtp->uri, tmp_url, url_len);
+ return;
+ }
+
+ int ftp_cmd_handler_non_implemented(struct session *sess UNUSED, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env UNUSED)
+ {
+ return 0;
+ }
+
+ int ftp_cmd_handler_do_user(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ fstring_safe_dup(&ftp_ext->ftp_login_pri.username, &ftp_ext->ctrl_ext.cmd_line.arg_refer);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "session: %s, USER: %.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ return 0;
+ }
+
+ int ftp_cmd_handler_do_pass(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ fstring_safe_dup(&ftp_ext->ftp_login_pri.password, &ftp_ext->ctrl_ext.cmd_line.arg_refer);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "session: %s, PASS: %.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ return 0;
+ }
+
+ int ftp_cmd_handler_do_cwd(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ char tmp_path[PATH_MAX] = {};
+ snprintf(tmp_path, sizeof(tmp_path), "%.*s", IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ ftp_join_absolute_path(ftp_ext->ctrl_ext.current_working_dir, tmp_path, ftp_ext->ctrl_ext.current_working_dir, sizeof(ftp_ext->ctrl_ext.current_working_dir));
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "session: %s, CWD: %.*s, absolute pwd: %s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer), ftp_ext->ctrl_ext.current_working_dir);
+ return 0;
+ }
+
+ int ftp_parse_ipv4_port_style(const fstring *cmd_str, unsigned int *ipv4_net, unsigned short *port_net)
+ {
+ unsigned int fields[6] = {};
+ char raw_cmd_str_tmp[FTP_CMD_MAX_LENGTH + 1] = {};
+ size_t min_size = MIN(FTP_CMD_MAX_LENGTH, cmd_str->iov_len + 1);
+ snprintf(raw_cmd_str_tmp, min_size, "%.*s", (int)min_size, (char *)cmd_str->iov_base);
+ char only_integer_str_tmp[FTP_CMD_MAX_LENGTH + 1] = {};
+ if (sscanf(raw_cmd_str_tmp, "%*[a-zA-Z (]%255[0-9,]", only_integer_str_tmp) <= 0)
+ {
+ if (sscanf(raw_cmd_str_tmp, "%255[1234567890,]", only_integer_str_tmp) <= 0)
+ {
+ return -1;
+ }
+ }
+ int ret = sscanf(only_integer_str_tmp, "%u,%u,%u,%u,%u,%u", &fields[0], &fields[1], &fields[2], &fields[3], &fields[4], &fields[5]);
+ if (ret != 6)
+ {
+ return -1;
+ }
+ unsigned int dst_ip_host = (fields[0] << 24) | (fields[1] << 16) | (fields[2] << 8) | fields[3];
+ unsigned short dst_port_host = (fields[4] << 8) | fields[5];
+ *ipv4_net = htonl(dst_ip_host);
+ *port_net = htons(dst_port_host);
+ return 0;
+ }
+
+ void ftp_login_info_free(struct ftp_login_internal *login_info)
+ {
+ if (login_info->username.iov_base)
+ {
+ free((void *)login_info->username.iov_base);
+ }
+ if (login_info->password.iov_base)
+ {
+ free((void *)login_info->password.iov_base);
+ }
+ }
+
+ void ftp_exdata_dtp_free(struct ftp_dtp *dtp)
+ {
+ if (NULL == dtp)
+ {
+ return;
+ }
+ if (dtp->uri)
+ {
+ free((void *)dtp->uri);
+ }
+ free(dtp);
+ }
+
+ static void ftp_exdata_dtp_renew(struct ftp_decoder_exdata *ftp_ext)
+ {
+ if (ftp_ext->dtp)
+ {
+ ftp_exdata_dtp_free(ftp_ext->dtp);
+ }
+ ftp_ext->dtp = (struct ftp_dtp *)calloc(1, sizeof(struct ftp_dtp));
+ ftp_ext->dtp->mode = ftp_ext->ctrl_ext.mode;
+ ftp_ext->dtp->dir = ftp_ext->ctrl_ext.dir;
+ ftp_ext->dtp->cmd = ftp_ext->ctrl_ext.cmd_type;
+ ftp_ext->dtp->uri = NULL;
+ return;
+ }
+
+ /* PORT command only support IPv4, arg pattern: h1,h2,h3,h4,p1,p2 */
+ int ftp_cmd_handler_do_port(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.mode = FTP_TRANSFER_PORT;
+ unsigned int dst_ip_net;
+ unsigned short dst_port_net;
+ if (ftp_parse_ipv4_port_style(&ftp_ext->ctrl_ext.cmd_line.arg_refer, &dst_ip_net, &dst_port_net) < 0)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "do port cmd, parse error: %.*s", IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.cmd_line));
+ return -1;
+ }
+ struct ftp_session_addr ses_addr = {};
+ ftp_session_get_addr(sess, &ses_addr);
+ // in active mode, new data link src ip address is server ip address
+ ftp_make_hkey_v4(&ftp_ext->ctrl_ext.last_data_link_key, ses_addr.daddr4, dst_ip_net, dst_port_net);
+
+ ftp_create_new_datalink_hash_item(ftp_env, ftp_ext);
+ int thread_idx = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_idx, ftp_env, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "do port cmd: %.*s, parsed dip:%x, dport:%u, hashkey:%s",
+ IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer), dst_ip_net, dst_port_net,
+ ftp_hash_key_to_str(&ftp_ext->ctrl_ext.last_data_link_key));
+ return 0;
+ }
+
+ /* PASV command only support IPv4, the response like: '227 Entering Passive Mode (218,13,32,6,78,40).' */
+ int ftp_cmd_handler_do_pasv(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s' PASV command", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ static void ftp_update_datalink_dtp(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ struct ftp_datalink_htable *hitem = ftp_hash_search(&ftp_ext->ctrl_ext.last_data_link_key);
+ if (NULL == hitem)
+ {
+ STELLAR_LOG_WARN(ftp_env->logger_ref, FTP_MODULE_NAME, "data link not found, session:%s, key:%s",
+ session_get_readable_addr(sess), ftp_hash_key_to_str(&ftp_ext->ctrl_ext.last_data_link_key));
+ return;
+ }
+ hitem->ftp_ext->link_type = FTP_LINK_DATA;
+ hitem->ftp_ext->dtp = ftp_dtp_deep_clone(ftp_ext->dtp);
+ }
+
+ int ftp_cmd_handler_do_list(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_exdata_dtp_renew(ftp_ext);
+ // no uri for list command
+ ftp_update_datalink_dtp(sess, ftp_ext, ftp_env);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s' LIST command", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ int ftp_cmd_handler_do_stor(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.dir = FTP_TRANSFER_STOR;
+ ftp_exdata_dtp_renew(ftp_ext);
+ ftp_assemble_uri(sess, ftp_ext);
+ ftp_update_datalink_dtp(sess, ftp_ext, ftp_env);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s', STOR uri:%.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ return 0;
+ }
+
+ int ftp_cmd_handler_do_retr(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.dir = FTP_TRANSFER_RETR;
+ ftp_exdata_dtp_renew(ftp_ext);
+ ftp_assemble_uri(sess, ftp_ext);
+ ftp_update_datalink_dtp(sess, ftp_ext, ftp_env);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s', RETR uri:%.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ return 0;
+ }
+
+ int ftp_parse_eprt_ipport_style(const fstring *arg_str, struct in6_addr *ipd_addr, unsigned short *port_net, struct ftp_decoder *ftp_env)
+ {
+ unsigned int port_host;
+ int inet_proto;
+ char ip6_addr_str[INET6_ADDRSTRLEN] = {};
+ char raw_cmd_str_tmp[arg_str->iov_len + 1];
+ memset(raw_cmd_str_tmp, 0, sizeof(raw_cmd_str_tmp));
+ memcpy(raw_cmd_str_tmp, arg_str->iov_base, arg_str->iov_len);
+
+ char *save_ptr, *ptr;
+ const char *delim = " |\t";
+ ptr = strtok_r(raw_cmd_str_tmp, delim, &save_ptr);
+ if (NULL == ptr)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp EPRT command parse error: %.*s", IOVEC_PRINT_PTR(arg_str));
+ return -1;
+ }
+ inet_proto = atoi(ptr);
+ if (2 != inet_proto)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp EPRT command parse error: %.*s, not support inet: %d", IOVEC_PRINT_PTR(arg_str), inet_proto);
+ return -1;
+ }
+ ptr = strtok_r(NULL, delim, &save_ptr);
+ if (NULL == ptr)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp EPRT command parse error: %.*s", IOVEC_PRINT_PTR(arg_str));
+ return -1;
+ }
+ strncpy(ip6_addr_str, ptr, INET6_ADDRSTRLEN - 1);
+ ptr = strtok_r(NULL, delim, &save_ptr);
+ if (NULL == ptr)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp EPRT command parse error: %.*s", IOVEC_PRINT_PTR(arg_str));
+ return -1;
+ }
+ port_host = (unsigned int)atoi(ptr);
+
+ while (strtok_r(NULL, "|", &save_ptr))
+ ;
+ inet_pton(AF_INET6, ip6_addr_str, ipd_addr);
+ *port_net = htons((unsigned short)port_host);
+ return 0;
+ }
+
+ /* EPRT support IPv4 and IPv6, pattern: EPRT<space><d><net-prt><d><net-addr><d><tcp-port><d>
+ example:
+ EPRT |1|132.235.1.2|6275|
+ EPRT |2|1080::8:800:200C:417A|5282|
+ refer: https://datatracker.ietf.org/doc/html/rfc2428#section-2
+ */
+ int ftp_cmd_handler_do_eprt(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.mode = FTP_TRANSFER_PORT;
+ struct in6_addr ipd_addr;
+ unsigned short port_net;
+ // fstring_safe_dup(&ftp_ext->cmd_result.cmd_refer, &ftp_ext->parse_result.result_array[FTP_TRANS_MODE]);
+ if (ftp_parse_eprt_ipport_style(&ftp_ext->ctrl_ext.cmd_line.arg_refer, &ipd_addr, &port_net, ftp_env) < 0)
+ {
+ return -1;
+ }
+
+ struct ftp_session_addr ses_addr = {};
+ ftp_session_get_addr(sess, &ses_addr);
+ assert(ses_addr.af_inet == AF_INET6);
+ ftp_make_hkey_v6(&ftp_ext->ctrl_ext.last_data_link_key, (struct in6_addr *)&ses_addr.daddr6, &ipd_addr, port_net);
+
+ ftp_create_new_datalink_hash_item(ftp_env, ftp_ext);
+ int thread_id = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_id, ftp_env, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s': EPRT command, port:%u", session_get_readable_addr(sess), ntohs(port_net));
+ return 0;
+ }
+
+ /* LPRT support IPv4 and IPv6, pattern: LPRT af,hal,h1,h2,h3,h4...,pal,p1,p2...
+ example:
+ LPRT 6,16,32,2,81,131,67,131,0,0,0,0,0,0,81,131,67,131,2,4,7
+ refer: https://www.rfc-editor.org/rfc/rfc1639.html
+ */
+ int ftp_cmd_handler_do_lprt(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ // todo
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, " '%s': LPRT command not support yet!", session_get_readable_addr(sess));
+ return -1;
+ }
+
+ /* EPSV support IPv4 and IPv6,
+ refer: https://datatracker.ietf.org/doc/html/rfc2428#autoid-3
+ */
+ int ftp_cmd_handler_do_epsv(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s' EPSV command", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ /* LPSV support IPv4 and IPv6, response is 228 xxxx...
+ refer: https://www.rfc-editor.org/rfc/rfc1639.html
+ */
+ int ftp_cmd_handler_do_lpsv(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s' LPSV command", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ static const struct ftp_interact_cmd_parser g_ftp_c2s_cmd_tuple[] =
+ {
+ {FTP_COMMAND_SYST, "SYST", 4, ftp_cmd_handler_non_implemented},
+ {FTP_COMMAND_USER, "USER", 4, ftp_cmd_handler_do_user},
+ {FTP_COMMAND_PASS, "PASS", 4, ftp_cmd_handler_do_pass},
+ {FTP_COMMAND_CWD, "CWD", 3, ftp_cmd_handler_do_cwd},
+ {FTP_COMMAND_PORT, "PORT", 4, ftp_cmd_handler_do_port},
+ {FTP_COMMAND_PASV, "PASV", 4, ftp_cmd_handler_do_pasv},
+ {FTP_COMMAND_EPRT, "EPRT", 4, ftp_cmd_handler_do_eprt},
+ {FTP_COMMAND_EPSV, "EPSV", 4, ftp_cmd_handler_do_epsv},
+ {FTP_COMMAND_LPRT, "LPRT", 4, ftp_cmd_handler_do_lprt},
+ {FTP_COMMAND_LPSV, "LPSV", 4, ftp_cmd_handler_do_lpsv},
+ {FTP_COMMAND_LIST, "LIST", 4, ftp_cmd_handler_do_list},
+ {FTP_COMMAND_STOR, "STOR", 4, ftp_cmd_handler_do_stor},
+ {FTP_COMMAND_RETR, "RETR", 4, ftp_cmd_handler_do_retr},
+ {FTP_COMMAND_OTHERS, NULL, 0, NULL}};
+
+ const char *ftp_command_type_to_string(enum ftp_command cmd_type)
+ {
+ for (size_t i = 0; i < sizeof(g_ftp_c2s_cmd_tuple) / sizeof(struct ftp_interact_cmd_parser); i++)
+ {
+ if (g_ftp_c2s_cmd_tuple[i].cmd_type == cmd_type && cmd_type != FTP_COMMAND_OTHERS)
+ {
+ return g_ftp_c2s_cmd_tuple[i].cmd_name;
+ }
+ }
+ return "OTHERS";
+ }
+
+ int ftp_res_handler_do_220(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, " '%s': reply 220", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ int ftp_res_handler_do_200(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, " '%s': reply 200", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ /*
+ example: 227 Entering Passive Mode (218,13,32,6,78,40).
+ */
+ int ftp_res_handler_do_227(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.mode = FTP_TRANSFER_PASV;
+ unsigned int dst_ip_net;
+ unsigned short dst_port_net;
+ if (ftp_parse_ipv4_port_style(&ftp_ext->ctrl_ext.cmd_line.arg_refer, &dst_ip_net, &dst_port_net) < 0)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp_parse_ipv4_port_style parse error: %.*s",
+ IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.cmd_line));
+ return -1;
+ }
+
+ struct ftp_session_addr ses_addr = {};
+ ftp_session_get_addr(sess, &ses_addr);
+ // in passive mode, new data link src ip address is client ip address
+ ftp_make_hkey_v4(&ftp_ext->ctrl_ext.last_data_link_key, ses_addr.saddr4, dst_ip_net, dst_port_net);
+
+ ftp_create_new_datalink_hash_item(ftp_env, ftp_ext);
+ int thread_id = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_id, ftp_env, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s': response 227, %.*s parsed dip:0x%x, dport:%u, hashkey:%s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer),
+ ntohl(dst_ip_net), ntohs(dst_port_net),
+ ftp_hash_key_to_str(&ftp_ext->ctrl_ext.last_data_link_key));
+ return 0;
+ }
+
+ /*
+ LPSV reply,
+ example: 228 Entering Long Passive Mode (af, hal, h1, h2, h3,..., pal, p1, p2...)
+ refer: https://www.rfc-editor.org/rfc/rfc1639.html
+ */
+ int ftp_res_handler_do_228(struct session *sess, struct ftp_decoder_exdata *ftp_ext UNUSED, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.mode = FTP_TRANSFER_PASV;
+ // todo
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s': response 228 command not support yet!", session_get_readable_addr(sess));
+ return 0;
+ }
+
+ int ftp_parse_ipv6_port_style(const fstring *cmd_str, unsigned short *port_net)
+ {
+ unsigned int port_host;
+ // char raw_cmd_str_tmp[cmd_str->iov_len + 1];
+ char raw_cmd_str_tmp[FTP_CMD_MAX_LENGTH + 1];
+ memset(raw_cmd_str_tmp, 0, sizeof(raw_cmd_str_tmp));
+ memcpy(raw_cmd_str_tmp, cmd_str->iov_base, cmd_str->iov_len);
+
+ char only_integer_str_tmp[FTP_CMD_MAX_LENGTH + 1];
+ memset(only_integer_str_tmp, 0, sizeof(only_integer_str_tmp));
+ if (sscanf(raw_cmd_str_tmp, "%*[a-zA-Z (|]%255[0-9]", only_integer_str_tmp) <= 0)
+ {
+ if (sscanf(raw_cmd_str_tmp, "|||%255s|", only_integer_str_tmp) <= 0)
+ {
+ return -1;
+ }
+ }
+ int ret = sscanf(only_integer_str_tmp, "%u", &port_host);
+ if (ret != 1)
+ {
+ return -1;
+ }
+ *port_net = htons((unsigned short)port_host);
+ return 0;
+ }
+ /*
+ reply of EPSV command,
+ example: 229 229 Entering Extended Passive Mode (|||20987|)
+ refer: https://datatracker.ietf.org/doc/html/rfc2428#autoid-3
+ */
+ int ftp_res_handler_do_229(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ ftp_ext->ctrl_ext.mode = FTP_TRANSFER_PASV;
+ unsigned short port_net;
+ if (ftp_parse_ipv6_port_style(&ftp_ext->ctrl_ext.cmd_line.arg_refer, &port_net) < 0)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s': response 229 parse error, %.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ return -1;
+ }
+
+ struct ftp_session_addr ses_addr = {};
+ ftp_session_get_addr(sess, &ses_addr);
+ if (AF_INET6 != ses_addr.af_inet && AF_INET != ses_addr.af_inet)
+ {
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "session: %s, parse response 229 error! %.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.arg_refer));
+ return -1;
+ }
+ // in passive mode, new data link src ip address is client ip address
+ if (AF_INET == ses_addr.af_inet)
+ {
+ ftp_make_hkey_v4(&ftp_ext->ctrl_ext.last_data_link_key, ses_addr.saddr4, ses_addr.daddr4, port_net);
+ }
+ else
+ {
+ ftp_make_hkey_v6(&ftp_ext->ctrl_ext.last_data_link_key, (struct in6_addr *)&ses_addr.saddr6, (struct in6_addr *)&ses_addr.daddr6, port_net);
+ }
+ ftp_create_new_datalink_hash_item(ftp_env, ftp_ext);
+ int thread_id = module_manager_get_thread_id(ftp_env->mod_mgr_ref);
+ ftp_decoder_stat_incrby(thread_id, ftp_env, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "'%s': EPSV response 229, port is:%u", session_get_readable_addr(sess), ntohs(port_net));
+ return 0;
+ }
+
+ static const struct ftp_interact_reply_parser g_ftp_s2c_cmd_tuple[] =
+ {
+ {FTP_REPLY_CMD_OK, "220", 3, ftp_res_handler_do_220},
+ {FTP_REPLY_SERVICE_READY, "200", 3, ftp_res_handler_do_200},
+ {FTP_REPLY_ENTER_PASV_MODE, "227", 3, ftp_res_handler_do_227},
+ {FTP_REPLY_ENTER_LONG_PASV_MODE, "228", 3, ftp_res_handler_do_228},
+ {FTP_REPLY_ENTER_EXTEND_PASSIVE_MODE, "229", 3, ftp_res_handler_do_229},
+ {FTP_REPLY_OTHRES, NULL, 0, NULL}};
+
+ const char *ftp_reply_type_to_string(enum ftp_reply_code reply_type)
+ {
+ for (size_t i = 0; i < sizeof(g_ftp_s2c_cmd_tuple) / sizeof(struct ftp_interact_reply_parser); i++)
+ {
+ if (g_ftp_s2c_cmd_tuple[i].reply_code_type == reply_type && reply_type != FTP_REPLY_OTHRES)
+ {
+ return g_ftp_s2c_cmd_tuple[i].reply_code;
+ }
+ }
+ return "OTHERS";
+ }
+
+ int ftp_ctrl_identify(struct session *sess UNUSED, const char *payload, size_t len, enum flow_type curdir)
+ {
+#if 0
+ if(ftp_ctrl_identify_by_addr(sess)){
+ return 1;
+ }
+#endif
+ return ftp_ctrl_identify_by_payload(payload, len, curdir);
+ }
+
+ struct ftp_decoder_exdata *ftp_data_identify(struct session *sess, struct ftp_decoder *ftp_env)
+ {
+ ftp_hash_key_t hash_key = {};
+ ftp_session_get_addr(sess, &hash_key);
+ if (hash_key.af_inet == 0)
+ {
+ STELLAR_LOG_WARN(ftp_env->logger_ref, FTP_MODULE_NAME, "data link identify(): get session addr fail: %s", session_get_readable_addr(sess));
+ return NULL;
+ }
+ /* sport is random value in passive mode, so set sport=0 for all active and passive mode */
+ hash_key.sport = 0;
+ struct ftp_datalink_htable *hitem = ftp_hash_search(&hash_key);
+ if (NULL == hitem)
+ {
+ STELLAR_LOG_DEBUG(ftp_env->logger_ref, FTP_MODULE_NAME, "not ftp session. hashkey: %s, session: %s",
+ ftp_hash_key_to_str(&hash_key), session_get_readable_addr(sess));
+ return NULL;
+ }
+ struct ftp_decoder_exdata *data_ext = hitem->ftp_ext;
+ return data_ext;
+ }
+
+ static const struct ftp_interact_cmd_parser *ftp_fetch_cmd_parser(const struct ftp_interact_cmd_parser *table,
+ size_t table_item_max, const struct ftp_interact_line *cmd_result)
+ {
+ for (size_t i = 0; i < table_item_max && table[i].cmd_name != NULL; i++)
+ {
+ if (0 == strncmp(table[i].cmd_name, (char *)cmd_result->cmd_refer.iov_base, table[i].cmd_len))
+ {
+ return &table[i];
+ }
+ }
+ return &table[table_item_max - 1];
+ }
+
+ static const struct ftp_interact_reply_parser *ftp_fetch_reply_parser(const struct ftp_interact_reply_parser *table,
+ size_t table_item_max, const struct ftp_interact_line *cmd_result)
+ {
+ for (size_t i = 0; i < table_item_max && table[i].reply_code != NULL; i++)
+ {
+ if (0 == strncmp(table[i].reply_code, (char *)cmd_result->cmd_refer.iov_base, table[i].reply_len))
+ {
+ return &table[i];
+ }
+ }
+ return &table[table_item_max - 1];
+ }
+
+ int ftp_command_process(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ const struct ftp_interact_cmd_parser *cmd_parser = NULL;
+ cmd_parser = ftp_fetch_cmd_parser(g_ftp_c2s_cmd_tuple,
+ sizeof(g_ftp_c2s_cmd_tuple) / sizeof(struct ftp_interact_cmd_parser),
+ &ftp_ext->ctrl_ext.cmd_line);
+ ftp_ext->ctrl_ext.cmd_type = cmd_parser->cmd_type;
+ if (NULL == cmd_parser->cmd_handler)
+ {
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "fetch parser() failed, '%s', not support cmd %.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.cmd_line));
+ return 0;
+ }
+ return cmd_parser->cmd_handler(sess, ftp_ext, ftp_env);
+ }
+
+ int ftp_reply_process(struct session *sess, struct ftp_decoder_exdata *ftp_ext, struct ftp_decoder *ftp_env)
+ {
+ const struct ftp_interact_reply_parser *reply_parser = NULL;
+
+ reply_parser = ftp_fetch_reply_parser(g_ftp_s2c_cmd_tuple,
+ sizeof(g_ftp_s2c_cmd_tuple) / sizeof(struct ftp_interact_reply_parser),
+ &ftp_ext->ctrl_ext.cmd_line);
+ ftp_ext->ctrl_ext.reply_code = reply_parser->reply_code_type;
+ if (NULL == reply_parser->reply_handler)
+ {
+ STELLAR_LOG_INFO(ftp_env->logger_ref, FTP_MODULE_NAME, "fetch parser() failed, '%s', not support cmd %.*s",
+ session_get_readable_addr(sess), IOVEC_PRINT(ftp_ext->ctrl_ext.cmd_line.cmd_line));
+ return 0;
+ }
+ return reply_parser->reply_handler(sess, ftp_ext, ftp_env);
+ }
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/decoders/ftp/ftp_decoder_stat.c b/decoders/ftp/ftp_decoder_stat.c
new file mode 100644
index 0000000..f06e5bf
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_stat.c
@@ -0,0 +1,55 @@
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include <assert.h>
+#include <fieldstat/fieldstat_easy.h>
+#include "ftp_decoder_stat.h"
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+
+ static const struct ftpd_stat_name_id_tuple g_ftpd_stat_tuple[] =
+ {
+ {FTPD_STAT_CTRL_LINK_NEW, "clink_open"},
+ {FTPD_STAT_CTRL_LINK_FREE, "clink_close"},
+ {FTPD_STAT_DATA_LINK_NEW, "dlink_open"},
+ {FTPD_STAT_DATA_LINK_FREE, "dlink_close"},
+ {FTPD_STAT_NEGOTIATE_DATA_LINK, "negotiate_dlink"},
+ {FTPD_STAT_CTRL_LINK_BYTES_C2S, "clink_bytes_c2s"},
+ {FTPD_STAT_CTRL_LINK_BYTES_S2C, "clink_bytes_s2c"},
+ {FTPD_STAT_DATA_LINK_BYTES_C2S, "dlink_bytes_c2s"},
+ {FTPD_STAT_DATA_LINK_BYTES_S2C, "dlink_bytes_s2c"},
+ {FTPD_STAT_DATA_LINK_HTABLE_ITEMS, "dlink_hash_items"},
+ {FTPD_STAT_CTRL_CMD, "ctrl_commands"},
+ };
+
+ void ftp_decoder_stat_incrby(int thread_idx, struct ftp_decoder *ftp_env, enum ftp_decoder_stat_type stattype, long long increment)
+ {
+ if (ftp_env->stat.fs4_instance)
+ {
+ fieldstat_easy_counter_incrby(ftp_env->stat.fs4_instance, thread_idx, ftp_env->stat.fs4_counter_id[stattype], NULL, 0, increment);
+ }
+ }
+
+ int ftp_decoder_stat_init(struct module_manager *mod_mgr, struct ftp_decoder *ftp_env)
+ {
+ assert(sizeof(g_ftpd_stat_tuple) / sizeof(struct ftpd_stat_name_id_tuple) == FTPD_STAT_MAX);
+ int thread_count = module_manager_get_max_thread_num(mod_mgr);
+ ftp_env->stat.fs4_instance = fieldstat_easy_new(thread_count, FTP_DECODER_FIELDSTAT_NAME, NULL, 0);
+ for (int i = 0; i < FTPD_STAT_MAX; i++)
+ {
+ ftp_env->stat.fs4_counter_id[i] = fieldstat_easy_register_counter(ftp_env->stat.fs4_instance, g_ftpd_stat_tuple[i].name);
+ }
+ fieldstat_easy_enable_auto_output(ftp_env->stat.fs4_instance, FTP_DECODER_FIELDSTAT_OUTPUT_FILE, FTP_DECODER_FIELDSTAT_OUTPUT_INTERVAL);
+ return 0;
+ }
+
+ void ftp_decoder_stat_free(struct ftp_decoder *ftp_env)
+ {
+ fieldstat_easy_free(ftp_env->stat.fs4_instance);
+ ftp_env->stat.fs4_instance = NULL;
+ }
+
+#ifdef __cplusplus
+}
+#endif \ No newline at end of file
diff --git a/decoders/ftp/ftp_decoder_stat.h b/decoders/ftp/ftp_decoder_stat.h
new file mode 100644
index 0000000..b477950
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_stat.h
@@ -0,0 +1,43 @@
+#pragma once
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <fieldstat/fieldstat_easy.h>
+#include "stellar/ftp.h"
+
+ enum ftp_decoder_stat_type
+ {
+ FTPD_STAT_CTRL_LINK_NEW = 0,
+ FTPD_STAT_CTRL_LINK_FREE,
+ FTPD_STAT_DATA_LINK_NEW,
+ FTPD_STAT_DATA_LINK_FREE,
+ FTPD_STAT_NEGOTIATE_DATA_LINK, // Negotiating by PORT or PASV command in ctrl link
+ FTPD_STAT_CTRL_LINK_BYTES_C2S,
+ FTPD_STAT_CTRL_LINK_BYTES_S2C,
+ FTPD_STAT_DATA_LINK_BYTES_C2S,
+ FTPD_STAT_DATA_LINK_BYTES_S2C,
+ FTPD_STAT_DATA_LINK_HTABLE_ITEMS,
+ FTPD_STAT_CTRL_CMD,
+ FTPD_STAT_MAX,
+ };
+
+ struct ftpd_stat_name_id_tuple
+ {
+ enum ftp_decoder_stat_type type;
+ const char *name;
+ };
+
+ struct ftp_decoder_stat
+ {
+ struct fieldstat_easy *fs4_instance;
+ int fs4_counter_id[FTPD_STAT_MAX];
+ };
+ int ftp_decoder_stat_init(struct module_manager *mod_mgr, struct ftp_decoder *ftp_env);
+ void ftp_decoder_stat_free(struct ftp_decoder *ftp_env);
+ void ftp_decoder_stat_incrby(int thread_idx, struct ftp_decoder *ftp_env, enum ftp_decoder_stat_type stattype, long long increment);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/decoders/ftp/ftp_decoder_util.c b/decoders/ftp/ftp_decoder_util.c
new file mode 100644
index 0000000..b0e1d3c
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_util.c
@@ -0,0 +1,400 @@
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+#include "ftp_decoder_hash.h"
+#include "stellar/session.h"
+#include "stellar/packet.h"
+#include <ctype.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <libgen.h>
+
+ void fstring_dup(fstring *dst, const fstring *src)
+ {
+ dst->iov_base = (char *)calloc(1, src->iov_len + 1);
+ assert(dst->iov_base);
+ memcpy((void *)dst->iov_base, src->iov_base, src->iov_len);
+ dst->iov_len = src->iov_len;
+ }
+ void fstring_safe_dup(fstring *dst, const fstring *src)
+ {
+ if (dst->iov_base)
+ {
+ free(dst->iov_base); // free it if exist
+ }
+ fstring_dup(dst, src);
+ }
+
+ void ftp_cmd_line_parse(const char *payload, size_t len, struct ftp_interact_line *cmd_line, int delim)
+ {
+ cmd_line->cmd_line.iov_base = (char *)payload;
+ cmd_line->cmd_line.iov_len = len;
+
+ const char *p = (char *)memchr(payload, delim, len);
+ if (NULL == p)
+ {
+ cmd_line->cmd_refer.iov_base = (char *)payload;
+ cmd_line->cmd_refer.iov_len = len;
+ cmd_line->arg_refer.iov_base = NULL;
+ cmd_line->arg_refer.iov_len = 0;
+ }
+ else
+ {
+ cmd_line->cmd_refer.iov_base = (char *)payload;
+ cmd_line->cmd_refer.iov_len = (p - payload);
+ cmd_line->arg_refer.iov_base = (char *)p + 1;
+ cmd_line->arg_refer.iov_len = len - (p + 1 - payload);
+ }
+ }
+
+ size_t ftp_strip_crlf(const char *payload, size_t len)
+ {
+ size_t new_len = 0;
+ for (size_t i = 0; i < len; i++, new_len++)
+ {
+ if ((payload[i] == '\r' || payload[i] == '\n'))
+ {
+ break;
+ }
+ }
+ return new_len;
+ }
+
+ int ftp_cmd_readline(struct ftp_interact_line *cmd_line, const char *payload, size_t len)
+ {
+ memset(cmd_line, 0, sizeof(struct ftp_interact_line));
+ size_t strip_crlf_len = ftp_strip_crlf(payload, len);
+ if (strip_crlf_len == 0)
+ {
+ return -1;
+ }
+ ftp_cmd_line_parse(payload, strip_crlf_len, cmd_line, ' ');
+ return 0;
+ }
+
+ void ftp_strtolower(char *str, size_t len)
+ {
+ for (size_t i = 0; i < len; i++)
+ {
+ str[i] = tolower(str[i]);
+ }
+ }
+
+ int ftp_mkdir_p(const char *path, mode_t mode)
+ {
+ struct stat st;
+ errno = 0;
+
+ /* Try to make the directory */
+ if (mkdir(path, mode) == 0)
+ return 0;
+
+ /* If it fails for any reason but EEXIST, fail */
+ if (errno != EEXIST)
+ return -1;
+
+ /* Check if the existing path is a directory */
+ if (stat(path, &st) != 0)
+ return -1;
+
+ /* If not, fail with ENOTDIR */
+ if (!S_ISDIR(st.st_mode))
+ {
+ errno = ENOTDIR;
+ return -1;
+ }
+
+ errno = 0;
+ return 0;
+ }
+
+ struct ftp_dtp *ftp_dtp_deep_clone(const struct ftp_dtp *src)
+ {
+ if (NULL == src)
+ {
+ return NULL;
+ }
+ struct ftp_dtp *new_dtp = (struct ftp_dtp *)calloc(1, sizeof(struct ftp_dtp));
+ memcpy(new_dtp, src, sizeof(struct ftp_dtp));
+ if (src->uri)
+ {
+ new_dtp->uri = strdup(src->uri);
+ }
+ else
+ {
+ new_dtp->uri = NULL;
+ }
+ return new_dtp;
+ }
+
+ /*
+ from ctrl link to data link.
+ */
+ struct ftp_decoder_exdata *ftp_exdata_deep_clone(const struct ftp_decoder_exdata *src)
+ {
+ struct ftp_decoder_exdata *new_ext = (struct ftp_decoder_exdata *)calloc(1, sizeof(struct ftp_decoder_exdata));
+ memcpy(new_ext, src, sizeof(struct ftp_decoder_exdata));
+ fstring_dup(&new_ext->ftp_login_pri.username, &src->ftp_login_pri.username);
+ fstring_dup(&new_ext->ftp_login_pri.password, &src->ftp_login_pri.password);
+ new_ext->dtp = NULL; // update in retr/stor commands
+ new_ext->reference = 1;
+ return new_ext;
+ }
+
+ static void ftp_set_tcp_addr(const struct tcphdr *tcph, struct ftp_session_addr *addr, enum flow_type fdir)
+ {
+ if (FLOW_TYPE_C2S == fdir)
+ {
+ addr->sport = 0; // tcph->th_sport;
+ addr->dport = tcph->th_dport;
+ }
+ else
+ {
+ addr->sport = 0; // tcph->th_dport;
+ addr->dport = tcph->th_sport;
+ }
+ }
+ static void ftp_set_ipv4_addr(const struct ip *ip4h, struct ftp_session_addr *addr, enum flow_type fdir)
+ {
+ addr->af_inet = AF_INET;
+ if (FLOW_TYPE_C2S == fdir)
+ {
+ addr->saddr4 = ip4h->ip_src.s_addr;
+ addr->daddr4 = ip4h->ip_dst.s_addr;
+ }
+ else
+ {
+ addr->saddr4 = ip4h->ip_dst.s_addr;
+ addr->daddr4 = ip4h->ip_src.s_addr;
+ }
+ }
+ static void ftp_set_ipv6_addr(const struct ip6_hdr *ip6h, struct ftp_session_addr *addr, enum flow_type fdir)
+ {
+ addr->af_inet = AF_INET6;
+ if (FLOW_TYPE_C2S == fdir)
+ {
+ memcpy(&addr->saddr6, &ip6h->ip6_src, sizeof(struct in6_addr));
+ memcpy(&addr->daddr6, &ip6h->ip6_dst, sizeof(struct in6_addr));
+ }
+ else
+ {
+ memcpy(&addr->saddr6, &ip6h->ip6_dst, sizeof(struct in6_addr));
+ memcpy(&addr->daddr6, &ip6h->ip6_src, sizeof(struct in6_addr));
+ }
+ }
+
+ void ftp_session_get_addr(const struct session *sess, struct ftp_session_addr *addr)
+ {
+ if (sess == NULL || addr == NULL)
+ {
+ return;
+ }
+ enum flow_type fdir = session_get_flow_type(sess);
+ const struct packet *raw_pkt = session_get_first_packet(sess, fdir);
+ if (NULL == raw_pkt)
+ {
+ addr->af_inet = 0;
+ return;
+ }
+
+ int count = packet_get_layer_count(raw_pkt);
+ for (int i = count - 1; i >= 0; i--)
+ {
+ const struct layer *layer = packet_get_layer_by_idx(raw_pkt, i);
+ if (layer->proto == LAYER_PROTO_TCP)
+ {
+ ftp_set_tcp_addr(layer->hdr.tcp, addr, fdir);
+ }
+ else if (layer->proto == LAYER_PROTO_IPV4)
+ {
+ ftp_set_ipv4_addr(layer->hdr.ip4, addr, fdir);
+ break;
+ }
+ else if (layer->proto == LAYER_PROTO_IPV6)
+ {
+ ftp_set_ipv6_addr(layer->hdr.ip6, addr, fdir);
+ break;
+ }
+ }
+ }
+
+ int ftp_ctrl_identify_by_payload(const char *payload, size_t len, enum flow_type curdir)
+ {
+ if (NULL == payload || len < FTP_IDENTIRY_MIN_LEN)
+ {
+ return 0;
+ }
+ if (curdir == FLOW_TYPE_C2S)
+ {
+ if ((strncasecmp(payload, "USER", 4) == 0) ||
+ (strncasecmp(payload, "OPTS", 4) == 0) ||
+ (strncasecmp(payload, "SYST", 4) == 0) ||
+ (strncasecmp(payload, "STAT", 4) == 0))
+ {
+ return 1;
+ }
+ }
+ else
+ {
+ if (memcmp(payload, "220", 3) == 0)
+ {
+ int tmplen = MIN(len, FTP_IDENTIRY_MAX_LEN);
+ char tmp_buffer[tmplen];
+ memcpy(tmp_buffer, payload, tmplen);
+ ftp_strtolower(tmp_buffer, tmplen);
+ if ((memmem(tmp_buffer, tmplen, "ftp", 3) != NULL))
+ {
+ return 1;
+ }
+ }
+ }
+ return 0;
+ }
+#if 0
+ /* return value need free after used */
+ const char *ftp_path_update(const char *current_path, const char *new_cwd_path)
+ {
+ size_t current_path_len = current_path ? strlen(current_path) : 0;
+ size_t new_path_len = new_cwd_path ? strlen(new_cwd_path) : 0;
+
+ if (NULL == new_cwd_path || new_path_len == 0)
+ {
+ return strdup(current_path);
+ }
+
+ if (NULL == current_path || current_path_len == 0)
+ {
+ if (new_cwd_path)
+ {
+ if ('/' == new_cwd_path[0])
+ {
+ return strdup(new_cwd_path);
+ }
+ else
+ {
+ size_t tmp_len = strlen(new_cwd_path) + 2;
+ char *new_path = (char *)calloc(1, tmp_len);
+ snprintf(new_path, tmp_len, "/%s", new_cwd_path);
+ return new_path;
+ }
+ }
+ return NULL;
+ }
+
+ char *new_path = (char *)calloc(1, current_path_len + new_path_len + 1);
+ if (current_path[0] == '/')
+ {
+ snprintf(new_path, PATH_MAX, "%s/%s", current_path, new_cwd_path);
+ }
+ else
+ {
+ snprintf(new_path, PATH_MAX, "/%s/%s", current_path, new_cwd_path);
+ }
+ return new_path;
+ }
+#endif
+
+ static void ftp_normalize_path(const char *input, char *output, size_t size)
+ {
+ if (!input || !output || size == 0)
+ {
+ if (output)
+ {
+ output[0] = '\0';
+ }
+ return;
+ }
+
+ char *stack[PATH_MAX]; // Use pointers for components, no deep copying.
+ int stack_index = 0;
+
+ // Temporary buffer for tokenization
+ char temp_path[PATH_MAX];
+ strncpy(temp_path, input, sizeof(temp_path) - 1);
+ temp_path[sizeof(temp_path) - 1] = '\0';
+
+ // Tokenize the path and process components
+ char *token = strtok(temp_path, "/");
+ while (token)
+ {
+ if (strcmp(token, "..") == 0)
+ {
+ if (stack_index > 0)
+ stack_index--; // Pop from stack
+ }
+ else if (strcmp(token, ".") != 0 && strlen(token) > 0)
+ {
+ stack[stack_index++] = token; // Push valid component
+ }
+ token = strtok(NULL, "/");
+ }
+
+ // Build the normalized path
+ size_t len = 0;
+ output[0] = '\0';
+ for (int i = 0; i < stack_index; i++)
+ {
+ len += snprintf(output + len, size - len, "/%s", stack[i]);
+ if (len >= size)
+ break; // Avoid buffer overflow
+ }
+
+ // Ensure at least "/" is returned
+ if (stack_index == 0)
+ strncpy(output, "/", size - 1);
+
+ output[size - 1] = '\0'; // Null-terminate the output
+ }
+
+ /*
+ * According to the cwd, the server address, the file name of stor/retr command,
+ * concatenated them into a complete absolute uri path.
+ */
+ void ftp_join_absolute_path(const char *dir, const char *file, char *result, size_t size)
+ {
+ if (!dir || !file || !result || size == 0)
+ {
+ if (result)
+ {
+ result[0] = '\0';
+ }
+ return;
+ }
+ char combined_path[PATH_MAX] = {};
+ // If file is absolute path, use it directly
+ if (file[0] == '/')
+ {
+ strncpy(combined_path, file, sizeof(combined_path) - 1);
+ combined_path[sizeof(combined_path) - 1] = '\0';
+ }
+ else
+ {
+ if (dir[0] == '\0')
+ {
+ snprintf(combined_path, sizeof(combined_path), "/%s", file);
+ }
+ else
+ {
+ // Otherwise, join dir and file
+ if (dir[strlen(dir) - 1] == '/')
+ {
+ snprintf(combined_path, sizeof(combined_path), "%s%s", dir, file);
+ }
+ else
+ {
+ snprintf(combined_path, sizeof(combined_path), "%s/%s", dir, file);
+ }
+ }
+ }
+ // Normalize the combined path
+ ftp_normalize_path(combined_path, result, size);
+ }
+
+#ifdef __cplusplus
+}
+#endif \ No newline at end of file
diff --git a/decoders/ftp/ftp_decoder_util.h b/decoders/ftp/ftp_decoder_util.h
new file mode 100644
index 0000000..004b922
--- /dev/null
+++ b/decoders/ftp/ftp_decoder_util.h
@@ -0,0 +1,35 @@
+#pragma once
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <string.h>
+#include <bits/types/struct_iovec.h>
+#include "stellar/ftp.h"
+
+#ifndef MAX
+#define MAX(a, b) ((a) >= (b) ? (a) : (b))
+#endif
+#ifndef MIN
+#define MIN(a, b) ((a) >= (b) ? (b) : (a))
+#endif
+
+#ifndef fstring
+ typedef struct iovec fstring;
+#endif
+
+ void fstring_dup(fstring *dst, const fstring *src);
+ void fstring_safe_dup(fstring *dst, const fstring *src);
+ void ftp_strtolower(char *str, size_t len);
+ int ftp_mkdir_p(const char *path, mode_t mode);
+ struct ftp_decoder_exdata *ftp_exdata_deep_clone(const struct ftp_decoder_exdata *src);
+ size_t ftp_strip_crlf(const char *payload, size_t len);
+ const char *ftp_command_type_to_string(enum ftp_command cmd_type);
+ const char *ftp_reply_type_to_string(enum ftp_reply_code reply_type);
+ struct ftp_dtp *ftp_dtp_deep_clone(const struct ftp_dtp *src);
+ void ftp_join_absolute_path(const char *dir, const char *file, char *result, size_t size);
+#ifdef __cplusplus
+}
+#endif \ No newline at end of file
diff --git a/decoders/ftp/ftp_module.c b/decoders/ftp/ftp_module.c
new file mode 100644
index 0000000..8819d60
--- /dev/null
+++ b/decoders/ftp/ftp_module.c
@@ -0,0 +1,192 @@
+#include <stdio.h>
+#include <assert.h>
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "stellar/session.h"
+#include "stellar/module.h"
+#include "stellar/mq.h"
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_stat.h"
+
+ static void ftp_on_msg_dispatch(int topic_id UNUSED, void *msg, on_msg_cb_func *on_msg_cb,
+ void *on_msg_cb_arg, void *dispatch_arg UNUSED)
+ {
+ assert(msg != NULL && on_msg_cb != NULL);
+ struct ftp_message *ftpmsg = (struct ftp_message *)msg;
+ switch (ftpmsg->topic_type)
+ {
+ case FTP_TOPIC_CTRL_REQ_LINE:
+ {
+ ftp_on_control_request_cb *on_ctrl_req_cb = (ftp_on_control_request_cb *)((void *)on_msg_cb);
+ on_ctrl_req_cb(ftpmsg->sess_ref, ftpmsg->ftp_ext_ref->ctrl_ext.cmd_type,
+ (const char *)ftpmsg->ftp_ext_ref->ctrl_ext.cmd_line.arg_refer.iov_base, ftpmsg->ftp_ext_ref->ctrl_ext.cmd_line.arg_refer.iov_len,
+ (const char *)ftpmsg->ftp_ext_ref->ctrl_ext.cmd_line.cmd_line.iov_base, ftpmsg->ftp_ext_ref->ctrl_ext.cmd_line.cmd_line.iov_len,
+ on_msg_cb_arg);
+ }
+ break;
+ case FTP_TOPIC_CTRL_RES_LINE:
+ {
+ ftp_on_control_response_cb *on_ctrl_res_cb = (ftp_on_control_response_cb *)((void *)on_msg_cb);
+ on_ctrl_res_cb(ftpmsg->sess_ref, ftpmsg->ftp_ext_ref->ctrl_ext.reply_code,
+ (const char *)ftpmsg->ftp_ext_ref->ctrl_ext.cmd_line.cmd_line.iov_base, ftpmsg->ftp_ext_ref->ctrl_ext.cmd_line.cmd_line.iov_len,
+ on_msg_cb_arg);
+ }
+ break;
+ case FTP_TOPIC_CTRL_DTP:
+ {
+ struct ftp_login local_login_info = {};
+ ftp_on_control_dtp_cb *on_ctrl_dtp_cb = (ftp_on_control_dtp_cb *)((void *)on_msg_cb);
+ local_login_info.username = (const char *)ftpmsg->ftp_ext_ref->ftp_login_pri.username.iov_base;
+ local_login_info.password = (const char *)ftpmsg->ftp_ext_ref->ftp_login_pri.password.iov_base;
+ on_ctrl_dtp_cb(ftpmsg->sess_ref, &local_login_info, ftpmsg->ftp_ext_ref->dtp, on_msg_cb_arg);
+ }
+ break;
+ case FTP_TOPIC_DATA_DTP:
+ {
+ struct ftp_login local_login_info = {};
+ ftp_on_data_connection_cb *on_data_dtp_cb = (ftp_on_data_connection_cb *)((void *)on_msg_cb);
+ local_login_info.username = (const char *)ftpmsg->ftp_ext_ref->ftp_login_pri.username.iov_base;
+ local_login_info.password = (const char *)ftpmsg->ftp_ext_ref->ftp_login_pri.password.iov_base;
+ on_data_dtp_cb(ftpmsg->sess_ref, &local_login_info, ftpmsg->ftp_ext_ref->dtp,
+ ftpmsg->ftp_ext_ref->data_ext.chunk, ftpmsg->ftp_ext_ref->data_ext.chunk_size,
+ ftpmsg->ftp_ext_ref->data_ext.offset, ftpmsg->ftp_ext_ref->data_ext.is_finished,
+ on_msg_cb_arg);
+ }
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ }
+
+ static int ftp_create_topic_nx(struct module_manager *mod_mgr, const char *topic_name)
+ {
+ struct mq_schema *mq_s = module_manager_get_mq_schema(mod_mgr);
+ assert(mq_s != NULL);
+ int topic_id = mq_schema_get_topic_id(mq_s, topic_name);
+ if (topic_id >= 0)
+ {
+ return topic_id;
+ }
+ topic_id = mq_schema_create_topic(mq_s, topic_name, (on_msg_dispatch_cb_func *)ftp_on_msg_dispatch,
+ NULL, ftp_msg_free_cb, NULL);
+ return topic_id;
+ }
+
+ static const struct ftp_topic_compose ftp_topic_schema[FTP_TOPIC_MAX] =
+ {
+ {FTP_TOPIC_CTRL_REQ_LINE, -1, "FTP_TOPIC_CTRL_REQ_LINE"},
+ {FTP_TOPIC_CTRL_RES_LINE, -1, "FTP_TOPIC_CTRL_RES_LINE"},
+ {FTP_TOPIC_CTRL_DTP, -1, "FTP_TOPIC_CTRL_DTP"},
+ {FTP_TOPIC_DATA_DTP, -1, "FTP_TOPIC_DATA_DTP"},
+ };
+
+ static struct ftp_topic_manager *ftp_topic_mgr_init(struct module_manager *mod_mgr)
+ {
+ struct ftp_topic_manager *http_topic_mgr = (struct ftp_topic_manager *)calloc(1, sizeof(struct ftp_topic_manager));
+ assert(http_topic_mgr != NULL);
+ struct ftp_topic_compose *topic_compose = http_topic_mgr->topic_compose;
+ for (int i = 0; i < (int)FTP_TOPIC_MAX; i++)
+ {
+ topic_compose[i].topic_type = ftp_topic_schema[i].topic_type;
+ topic_compose[i].topic_name = ftp_topic_schema[i].topic_name;
+ topic_compose[i].topic_id = ftp_create_topic_nx(mod_mgr, ftp_topic_schema[i].topic_name);
+ }
+ return http_topic_mgr;
+ }
+
+ static void ftp_topic_mgr_free(struct ftp_topic_manager *topic_mgr)
+ {
+ assert(topic_mgr != NULL);
+ FREE(topic_mgr);
+ }
+
+ static int ftp_subscribe_common(struct module_manager *mod_mgr, enum ftp_topic_type topic_type, void *cb, void *args)
+ {
+ struct module *ftp_mod = module_manager_get_module(mod_mgr, FTP_MODULE_NAME);
+ struct ftp_decoder *ftp_env = module_to_ftp_decoder(ftp_mod);
+ struct ftp_topic_manager *ftp_topic_mgr = ftp_env->ftp_topic_mgr;
+ if (ftp_topic_mgr == NULL)
+ {
+ ftp_topic_mgr = ftp_topic_mgr_init(mod_mgr);
+ ftp_env->ftp_topic_mgr = ftp_topic_mgr;
+ }
+ return mq_schema_subscribe(module_manager_get_mq_schema(mod_mgr),
+ ftp_topic_mgr->topic_compose[topic_type].topic_id,
+ (on_msg_cb_func *)cb, args);
+ }
+
+ int ftp_subscribe(struct ftp_decoder *ftp_decoder,
+ ftp_on_control_request_cb *on_control_request_cb,
+ ftp_on_control_response_cb *on_control_response_cb,
+ ftp_on_control_dtp_cb *on_control_dtp_cb,
+ ftp_on_data_connection_cb *on_data_connection_cb, void *arg)
+ {
+ assert(ftp_decoder != NULL);
+ struct module_manager *mod_mgr = ftp_decoder->mod_mgr_ref;
+ if (on_control_request_cb != NULL)
+ {
+ ftp_subscribe_common(mod_mgr, FTP_TOPIC_CTRL_REQ_LINE, (void *)on_control_request_cb, arg);
+ }
+ if (on_control_response_cb != NULL)
+ {
+ ftp_subscribe_common(mod_mgr, FTP_TOPIC_CTRL_RES_LINE, (void *)on_control_response_cb, arg);
+ }
+ if (on_control_dtp_cb != NULL)
+ {
+ ftp_subscribe_common(mod_mgr, FTP_TOPIC_CTRL_DTP, (void *)on_control_dtp_cb, arg);
+ }
+ if (on_data_connection_cb != NULL)
+ {
+ ftp_subscribe_common(mod_mgr, FTP_TOPIC_DATA_DTP, (void *)on_data_connection_cb, arg);
+ }
+
+ return 0;
+ }
+
+ struct module *ftp_init(struct module_manager *mod_mgr)
+ {
+ assert(mod_mgr != NULL);
+ struct ftp_decoder *ftp_env = (struct ftp_decoder *)calloc(1, sizeof(struct ftp_decoder));
+
+ ftp_decoder_stat_init(mod_mgr, ftp_env);
+ struct module *mod = module_new(FTP_MODULE_NAME, ftp_env);
+ ftp_env->mod_mgr_ref = mod_mgr;
+ ftp_env->logger_ref = module_manager_get_logger(mod_mgr);
+ assert(ftp_env->logger_ref != NULL);
+ struct module *sess_mod = module_manager_get_module(mod_mgr, SESSION_MANAGER_MODULE_NAME);
+ struct session_manager *sess_mgr = module_to_session_manager(sess_mod);
+ assert(sess_mgr != NULL);
+
+ struct ftp_topic_manager *ftp_topic_mgr = ftp_topic_mgr_init(mod_mgr);
+ assert(ftp_topic_mgr != NULL);
+ ftp_env->ftp_topic_mgr = ftp_topic_mgr;
+
+ session_manager_subscribe_tcp_stream(sess_mgr, ftp_on_tcp_stream_cb, ftp_env);
+ ftp_env->exdata_id = session_manager_new_session_exdata_index(sess_mgr, FTP_DECODER_DXDATA_NAME, ftp_decoder_exdata_free_cb, ftp_env);
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp init success");
+ return mod;
+ }
+
+ struct ftp_decoder *module_to_ftp_decoder(struct module *ftp_mod)
+ {
+ assert(ftp_mod);
+ return (struct ftp_decoder *)module_get_ctx(ftp_mod);
+ }
+
+ void ftp_exit(struct module_manager *mod_mgr UNUSED, struct module *mod)
+ {
+ assert(mod != NULL);
+ struct ftp_decoder *ftp_env = (struct ftp_decoder *)module_get_ctx(mod);
+ ftp_decoder_stat_free(ftp_env);
+ ftp_topic_mgr_free(ftp_env->ftp_topic_mgr);
+ STELLAR_LOG_FATAL(ftp_env->logger_ref, FTP_MODULE_NAME, "ftp exit!");
+ FREE(ftp_env);
+ module_free(mod);
+ }
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/decoders/ftp/version.map b/decoders/ftp/version.map
new file mode 100644
index 0000000..5acdfc3
--- /dev/null
+++ b/decoders/ftp/version.map
@@ -0,0 +1,12 @@
+VERS_3.0{
+global:
+ extern "C" {
+ ftp_init;
+ ftp_exit;
+ ftp_on_thread_init;
+ ftp_on_thread_exit;
+ ftp_subscribe;
+ module_to_ftp_decoder
+ };
+ local: *;
+};
diff --git a/include/stellar/ftp.h b/include/stellar/ftp.h
new file mode 100644
index 0000000..b55d004
--- /dev/null
+++ b/include/stellar/ftp.h
@@ -0,0 +1,88 @@
+#pragma once
+#include <stddef.h>
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "stellar/session.h"
+#include "stellar/module.h"
+
+ enum ftp_transfer_mode
+ {
+ FTP_TRANSFER_PASV, /* Entering Passive Mode (h1,h2,h3,h4,p1,p2). */
+ FTP_TRANSFER_PORT, /* PORT h1,h2,h3,h4,p1,p2 */
+ };
+
+ enum ftp_transfer_dir
+ {
+ FTP_TRANSFER_RETR, /* server transfer file to client */
+ FTP_TRANSFER_STOR, /* server accept file from client */
+ };
+
+ /* most commonly used command, parsing by command_line */
+ enum ftp_command
+ {
+ FTP_COMMAND_SYST,
+ FTP_COMMAND_USER,
+ FTP_COMMAND_PASS,
+ FTP_COMMAND_CWD,
+ FTP_COMMAND_PORT,
+ FTP_COMMAND_PASV,
+ FTP_COMMAND_EPRT,
+ FTP_COMMAND_EPSV,
+ FTP_COMMAND_LPRT,
+ FTP_COMMAND_LPSV,
+ FTP_COMMAND_LIST,
+ FTP_COMMAND_STOR,
+ FTP_COMMAND_RETR,
+ FTP_COMMAND_OTHERS,
+ };
+ enum ftp_reply_code
+ {
+ FTP_REPLY_CMD_OK = 200,
+ FTP_REPLY_SERVICE_READY = 220,
+ FTP_REPLY_CLOSE_CONTROL_CONNECTION = 221,
+ FTP_REPLY_CLOSE_DATA_CONNECTION = 226,
+ FTP_REPLY_ENTER_PASV_MODE = 227,
+ FTP_REPLY_ENTER_LONG_PASV_MODE = 228,
+ FTP_REPLY_ENTER_EXTEND_PASSIVE_MODE = 229,
+ FTP_REPLY_USER_LOGGED_IN = 230,
+ FTP_REPLY_USER_OK_NEED_PASS = 331,
+ FTP_REPLY_NOT_LOGIN = 530,
+ FTP_REPLY_OTHRES = 502, /* 502 Command not implemented. */
+ };
+
+ struct ftp_dtp /* data transfer process */
+ {
+ enum ftp_command cmd;
+ enum ftp_transfer_mode mode;
+ enum ftp_transfer_dir dir;
+ const char *uri; /* c string terminated by '\0' */
+ };
+
+ struct ftp_login
+ {
+ const char *username; /* c string terminated by '\0' */
+ const char *password; /* c string terminated by '\0' */
+ };
+
+ typedef void(ftp_on_control_request_cb)(struct session *sess, enum ftp_command cmd, const char *para, size_t para_len, const char *command_line, size_t cmd_line_len, void *arg);
+ typedef void(ftp_on_control_response_cb)(struct session *sess, enum ftp_reply_code reply_code, const char *reply_line, size_t reply_len, void *arg);
+ typedef void(ftp_on_control_dtp_cb)(struct session *sess, const struct ftp_login *login_info, const struct ftp_dtp *dtp, void *arg);
+
+ typedef void(ftp_on_data_connection_cb)(struct session *sess, const struct ftp_login *login_info, const struct ftp_dtp *dtp,
+ const char *chunk, size_t chunk_len, size_t offset, int is_finished, void *arg);
+
+#define FTP_MODULE_NAME "ftp_decoder"
+ struct ftp_decoder;
+ // use module_manager_get_module() to get ftp module
+ struct ftp_decoder *module_to_ftp_decoder(struct module *ftp_mod);
+ int ftp_subscribe(struct ftp_decoder *ftp_decoder,
+ ftp_on_control_request_cb *on_control_request_cb,
+ ftp_on_control_response_cb *on_control_response_cb,
+ ftp_on_control_dtp_cb *on_control_dtp_cb,
+ ftp_on_data_connection_cb *on_data_connection_cb, void *arg);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/infra/version.map b/infra/version.map
index 385a2a2..1b6e51a 100644
--- a/infra/version.map
+++ b/infra/version.map
@@ -79,5 +79,12 @@ global:
monitor_on_init;
monitor_on_exit;
+
+ ftp_init;
+ ftp_exit;
+ ftp_on_thread_init;
+ ftp_on_thread_exit;
+ ftp_subscribe;
+ module_to_ftp_decoder;
local: *;
};
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 11d1abc..2533e01 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -1,9 +1,10 @@
#add_subdirectory(packet_inject)
add_subdirectory(packet_tool)
add_subdirectory(session_debugger)
-add_subdirectory(lpi_plus)
+# add_subdirectory(lpi_plus)
#add_subdirectory(decoders/http)
#add_subdirectory(decoders/socks)
#add_subdirectory(decoders/stratum)
#add_subdirectory(decoders/session_flags)
-add_subdirectory(monitor) \ No newline at end of file
+add_subdirectory(monitor)
+add_subdirectory(decoders/ftp)
diff --git a/test/decoders/ftp/CMakeLists.txt b/test/decoders/ftp/CMakeLists.txt
new file mode 100644
index 0000000..25f8ee2
--- /dev/null
+++ b/test/decoders/ftp/CMakeLists.txt
@@ -0,0 +1,64 @@
+cmake_minimum_required (VERSION 2.8...3.10)
+include_directories(${CMAKE_SOURCE_DIR}/include/)
+include_directories(${CMAKE_SOURCE_DIR}/decoders/ftp/)
+
+add_executable(gtest_ftp_decoder gtest_ftp_decoder.cpp
+ ${CMAKE_SOURCE_DIR}/decoders/ftp/ftp_decoder_util.c
+ ${CMAKE_SOURCE_DIR}/decoders/ftp/ftp_decoder_proto.c
+ ${CMAKE_SOURCE_DIR}/decoders/ftp/ftp_decoder_hash.c
+ ${CMAKE_SOURCE_DIR}/decoders/ftp/ftp_decoder_stat.c
+ ${CMAKE_SOURCE_DIR}/decoders/ftp/ftp_decoder_entry.c)
+target_link_libraries(gtest_ftp_decoder gtest stellar_lib cjson-static )
+
+add_executable(ftp_gtest_main ftp_gtest_main.cpp ftp_gtest_module.cpp )
+target_link_libraries(ftp_gtest_main gtest stellar_lib fieldstat4 cjson-static
+ -Wl,--whole-archive stellar_lib ftp -Wl,--no-whole-archive
+ dl "-rdynamic"
+ gtest gmock)
+
+set(FTP_GTEST_MAIN ${CMAKE_CURRENT_BINARY_DIR}/ftp_gtest_main)
+set(TEST_PREFIX FTP)
+add_test(NAME ${TEST_PREFIX}.SETUP COMMAND sh -c "
+ mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/conf &&
+ mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/log &&
+ cp ${CMAKE_SOURCE_DIR}/conf/stellar.toml ${CMAKE_CURRENT_BINARY_DIR}/conf/ &&
+ cat ${CMAKE_SOURCE_DIR}/test/decoders/ftp/ftp_module.toml >> ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml &&
+ tomlq -t -i '.packet_io.pcap_path=\"-\"' ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml &&
+ tomlq -t -i '.packet_io.mode=\"pcaplist\"' ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml &&
+ tomlq -t -i '.packet_io.thread_num=1' ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml &&
+ tomlq -t -i '.session_manager.tcp_session_max=100' ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml &&
+ tomlq -t -i '.session_manager.udp_session_max=100' ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml &&
+ tomlq -t -i '.session_manager.tcp_reassembly.buffered_segments_max=1024' ${CMAKE_CURRENT_BINARY_DIR}/conf/stellar.toml
+ ")
+set_tests_properties(${TEST_PREFIX}.SETUP
+ PROPERTIES FIXTURES_SETUP ${TEST_PREFIX}.SETUP)
+
+set(FTP_TEST_JSON ftp_gtest_result.json)
+
+set(TEST_RUN_DIR ${CMAKE_CURRENT_BINARY_DIR})
+set(TEST_JSON_DIR ${CMAKE_SOURCE_DIR}/test/decoders/ftp/json)
+set(TEST_PCAP_DIR ${CMAKE_SOURCE_DIR}/test/decoders/ftp/pcap)
+
+set(FTP_BENCHMARK_CAESES
+ 01-ftp-port-upload-download_C2S
+ 01-ftp-port-upload-download
+ 01-ftp-port-upload-download_S2C
+ 02-ftp_v6_1
+ 03-ipv6_eport_upload_download
+ 04-ftp-banner-no-ftp-characters
+ 05-only-ctrl-link
+ 06-ftp_pasv-upload-download_C2S
+ 06-ftp_pasv-upload-download
+ 06-ftp_pasv-upload-download_S2C
+ 07-ftp-start-with-syst-command
+ 08-ftp_EPSV_229_ipv4
+ 10-ftp-sdedu
+ 11-ftp-sjtu
+)
+foreach(case_name ${FTP_BENCHMARK_CAESES})
+ add_test(NAME ${TEST_PREFIX}.${case_name} COMMAND sh -c "find ${TEST_PCAP_DIR}/ -name ${case_name}.pcap | sort -V | ${FTP_GTEST_MAIN} ${TEST_JSON_DIR}/${case_name}.json ${FTP_TEST_JSON}" ${TEST_RUN_DIR})
+ set_tests_properties(${TEST_PREFIX}.${case_name} PROPERTIES FIXTURES_REQUIRED ${TEST_PREFIX}.SETUP)
+endforeach()
+
+include(GoogleTest)
+gtest_discover_tests(gtest_ftp_decoder)
diff --git a/test/decoders/ftp/ftp_gtest_main.cpp b/test/decoders/ftp/ftp_gtest_main.cpp
new file mode 100644
index 0000000..64203c7
--- /dev/null
+++ b/test/decoders/ftp/ftp_gtest_main.cpp
@@ -0,0 +1,97 @@
+#include <stdio.h>
+#include <time.h>
+#include <unistd.h>
+#include <assert.h>
+#include <gtest/gtest.h>
+#include "stellar/stellar.h"
+#include "cJSON.h"
+
+static cJSON *load_result_from_jsonfile(const char *json_path)
+{
+ if (json_path == NULL)
+ return NULL;
+
+ long file_len = 0;
+ char *file_content = NULL;
+ FILE *fp = NULL;
+
+ fp = fopen(json_path, "r+");
+ if (NULL == fp)
+ {
+ return NULL;
+ }
+ fseek(fp, 0, SEEK_END);
+ file_len = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+ if (file_len == 0)
+ {
+ fclose(fp);
+ return NULL;
+ }
+ file_content = (char *)malloc(file_len + 1);
+ fread(file_content, file_len, 1, fp);
+ file_content[file_len] = '\0';
+ cJSON *load = cJSON_Parse(file_content);
+ free(file_content);
+ fclose(fp);
+ return load;
+}
+
+static void show_json_diff(cJSON *expect_json_root, cJSON *test_module_root)
+{
+ if (NULL == expect_json_root || NULL == test_module_root)
+ {
+ return;
+ }
+ cJSON *t_load = expect_json_root->child, *t_test = test_module_root->child;
+ int ret;
+ char *load_json_str = NULL;
+ char *result_json_str = NULL;
+ while (t_load != NULL && t_test != NULL)
+ {
+ ret = cJSON_Compare(t_load, t_test, 0);
+ if (ret != 1)
+ {
+ load_json_str = cJSON_Print(t_load);
+ printf("LOAD Diff:\n%s\n", load_json_str);
+ free(load_json_str);
+ result_json_str = cJSON_Print(t_test);
+ printf("TEST Diff:\n%s\n", result_json_str);
+ free(result_json_str);
+ }
+ t_load = t_load->next;
+ t_test = t_test->next;
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ if (argc != 3)
+ {
+ fprintf(stderr, "\nUsage: ./%s <expect-result> <test-module-result>\n\n", argv[0]);
+ exit(1);
+ }
+ ::testing::InitGoogleTest(&argc, (char **)argv);
+ const char *expect_json_path = argv[1];
+ const char *test_module_json_path = argv[2];
+
+ struct stellar *st = stellar_new("./conf/stellar.toml");
+ EXPECT_TRUE(st != NULL);
+ stellar_run(st);
+ stellar_free(st);
+
+ cJSON *expect_json_root = load_result_from_jsonfile(expect_json_path);
+ EXPECT_TRUE(expect_json_root != NULL);
+ cJSON *test_json_root = load_result_from_jsonfile(test_module_json_path);
+ EXPECT_TRUE(test_json_root != NULL);
+
+ int ret = cJSON_Compare(expect_json_root, test_json_root, 0);
+ EXPECT_EQ(1, ret);
+ if (ret != 1)
+ {
+ show_json_diff(expect_json_root, test_json_root);
+ }
+ cJSON_Delete(expect_json_root);
+ cJSON_Delete(test_json_root);
+ return ::testing::Test::HasFailure() ? 1 : 0;
+}
diff --git a/test/decoders/ftp/ftp_gtest_module.cpp b/test/decoders/ftp/ftp_gtest_module.cpp
new file mode 100644
index 0000000..fb174d7
--- /dev/null
+++ b/test/decoders/ftp/ftp_gtest_module.cpp
@@ -0,0 +1,250 @@
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "cJSON.h"
+#include "stellar/ftp.h"
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+#include <stellar/session.h>
+#include <stellar/exdata.h>
+#include <stellar/mq.h>
+#include <stellar/stellar.h>
+
+#define GTEST_RESULT_FILENAME "ftp_gtest_result.json"
+ static cJSON *gtest_ftp_json_root;
+
+ struct ftp_gtest_env
+ {
+ struct module_manager *mod_mgr;
+ int control_request_exdata_idx;
+ int control_response_exdata_idx;
+ int control_dtp_exdata_idx;
+ int data_dtp_exdata_idx;
+ };
+
+ struct ftp_gtest_ext
+ {
+ int cmd_index;
+ int reply_index;
+ int ctrl_dtp_index;
+ int is_data_link;
+ size_t data_link_bytes;
+ size_t data_link_offset;
+ int data_link_finished;
+ cJSON *lastest_data_dtp_obj;
+ cJSON *control_request_root;
+ cJSON *control_response_root;
+ cJSON *control_dtp_root; // multiple ctrl dtp
+ cJSON *data_dtp_root;
+ };
+
+ static void cjson_add_non_c_string(cJSON *root, const char *key, const char *value, size_t len)
+ {
+ char *str = (char *)calloc(1, len + 1);
+ memcpy(str, value, len);
+ cJSON_AddStringToObject(root, key, str);
+ free(str);
+ }
+
+ static void gtest_ftp_on_control_request_cb(struct session *sess, enum ftp_command cmd, const char *para, size_t para_len, const char *command_line, size_t cmd_line_len, void *arg)
+ {
+ (void)para;
+ (void)para_len;
+ (void)cmd_line_len;
+ struct ftp_gtest_env *env = (struct ftp_gtest_env *)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)session_get_exdata(sess, env->control_request_exdata_idx);
+ if (NULL == ext)
+ {
+ ext = (struct ftp_gtest_ext *)calloc(1, sizeof(struct ftp_gtest_ext));
+ session_set_exdata(sess, env->control_request_exdata_idx, ext);
+ ext->control_request_root = cJSON_CreateObject();
+ cJSON_AddStringToObject(ext->control_request_root, "topic", "control_request");
+ cJSON_AddStringToObject(ext->control_request_root, "session", session_get_readable_addr(sess));
+ }
+ char cmd_name[64] = {};
+ const char *cmd_readable_str = ftp_command_type_to_string(cmd);
+ snprintf(cmd_name, sizeof(cmd_name), "%d.%s", ext->cmd_index++, cmd_readable_str);
+ cjson_add_non_c_string(ext->control_request_root, cmd_name, command_line, cmd_line_len);
+ // printf("gtest_ftp_on_control_request_cb\n");
+ }
+
+ static void gtest_ftp_on_control_response_cb(struct session *sess, enum ftp_reply_code reply_code, const char *reply_line, size_t reply_len, void *arg)
+ {
+ struct ftp_gtest_env *env = (struct ftp_gtest_env *)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)session_get_exdata(sess, env->control_response_exdata_idx);
+ if (NULL == ext)
+ {
+ ext = (struct ftp_gtest_ext *)calloc(1, sizeof(struct ftp_gtest_ext));
+ session_set_exdata(sess, env->control_response_exdata_idx, ext);
+ ext->control_response_root = cJSON_CreateObject();
+ cJSON_AddStringToObject(ext->control_response_root, "topic", "control_response");
+ cJSON_AddStringToObject(ext->control_response_root, "session", session_get_readable_addr(sess));
+ }
+ char reply_name[64] = {};
+ const char *reply_readable_str = ftp_reply_type_to_string(reply_code);
+ snprintf(reply_name, sizeof(reply_name), "%d.%s", ext->reply_index++, reply_readable_str);
+ cjson_add_non_c_string(ext->control_response_root, reply_name, reply_line, reply_len);
+ // printf("gtest_ftp_on_control_response_cb\n");
+ }
+
+ static void gtest_ftp_dtp_add_to_json(cJSON *dtp_obj, const struct ftp_login *login_info, const struct ftp_dtp *dtp)
+ {
+ if (login_info) // S2C asymmetric flow
+ {
+ if (login_info->username && strlen(login_info->username) > 0)
+ {
+ cJSON_AddStringToObject(dtp_obj, "username", login_info->username);
+ }
+ if (login_info->password && strlen(login_info->password) > 0)
+ {
+ cJSON_AddStringToObject(dtp_obj, "password", login_info->password);
+ }
+ }
+ if (dtp) // S2C asymmetric flow
+ {
+ if (dtp->uri)
+ {
+ cJSON_AddStringToObject(dtp_obj, "uri", dtp->uri);
+ }
+ cJSON_AddStringToObject(dtp_obj, "dir", dtp->dir == FTP_TRANSFER_RETR ? "retr" : "stor");
+ cJSON_AddStringToObject(dtp_obj, "mode", dtp->mode == FTP_TRANSFER_PASV ? "pasv" : "port");
+ cJSON_AddStringToObject(dtp_obj, "cmd", ftp_command_type_to_string(dtp->cmd));
+ }
+ }
+
+ static void gtest_ftp_on_control_dtp_cb(struct session *sess, const struct ftp_login *login_info, const struct ftp_dtp *dtp, void *arg)
+ {
+ (void)sess;
+ (void)login_info;
+ (void)dtp;
+ (void)arg;
+ struct ftp_gtest_env *env = (struct ftp_gtest_env *)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)session_get_exdata(sess, env->control_dtp_exdata_idx);
+ if (NULL == ext)
+ {
+ ext = (struct ftp_gtest_ext *)calloc(1, sizeof(struct ftp_gtest_ext));
+ session_set_exdata(sess, env->control_dtp_exdata_idx, ext);
+ ext->control_dtp_root = cJSON_CreateObject();
+ cJSON_AddStringToObject(ext->control_dtp_root, "topic", "control_dtp");
+ cJSON_AddStringToObject(ext->control_dtp_root, "session", session_get_readable_addr(sess));
+ }
+ cJSON *dtp_obj = cJSON_CreateObject();
+ gtest_ftp_dtp_add_to_json(dtp_obj, login_info, dtp);
+ char dtp_index_name[16] = {};
+ snprintf(dtp_index_name, sizeof(dtp_index_name), "%d.dtp", ext->ctrl_dtp_index++);
+ cJSON_AddItemToObject(ext->control_dtp_root, dtp_index_name, dtp_obj);
+ // printf("gtest_ftp_on_control_dtp_cb\n");
+ }
+
+ static void gtest_ftp_on_data_connection_cb(struct session *sess, const struct ftp_login *login_info, const struct ftp_dtp *dtp,
+ const char *chunk, size_t chunk_len, size_t offset, int is_finished, void *arg)
+ {
+ (void)chunk;
+ struct ftp_gtest_env *env = (struct ftp_gtest_env *)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)session_get_exdata(sess, env->data_dtp_exdata_idx);
+ if (NULL == ext)
+ {
+ ext = (struct ftp_gtest_ext *)calloc(1, sizeof(struct ftp_gtest_ext));
+ session_set_exdata(sess, env->data_dtp_exdata_idx, ext);
+ ext->data_dtp_root = cJSON_CreateObject();
+ cJSON_AddStringToObject(ext->data_dtp_root, "topic", "data_dtp");
+ cJSON_AddStringToObject(ext->data_dtp_root, "session", session_get_readable_addr(sess));
+
+ if (dtp)
+ {
+ cJSON *dtp_obj = cJSON_CreateObject();
+ gtest_ftp_dtp_add_to_json(dtp_obj, login_info, dtp);
+ cJSON_AddItemToObject(ext->data_dtp_root, "dtp", dtp_obj);
+ }
+ }
+
+ ext->data_link_bytes += chunk_len;
+ ext->data_link_offset = offset;
+ ext->data_link_finished = is_finished;
+ if (1 == is_finished)
+ {
+ cJSON_AddNumberToObject(ext->data_dtp_root, "offset", ext->data_link_offset);
+ cJSON_AddNumberToObject(ext->data_dtp_root, "total_bytes", ext->data_link_bytes);
+ cJSON_AddNumberToObject(ext->data_dtp_root, "is_finished", ext->data_link_finished);
+ }
+ }
+
+ static void gtest_ftp_ctrl_req_exdata_free(int idx, void *ex_ptr, void *arg)
+ {
+ (void)idx;
+ (void)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)ex_ptr;
+ cJSON_AddItemToArray(gtest_ftp_json_root, ext->control_request_root);
+ free(ext);
+ }
+
+ static void gtest_ftp_ctrl_res_exdata_free(int idx, void *ex_ptr, void *arg)
+ {
+ (void)idx;
+ (void)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)ex_ptr;
+ cJSON_AddItemToArray(gtest_ftp_json_root, ext->control_response_root);
+ free(ext);
+ }
+
+ static void gtest_ftp_ctrl_dtp_exdata_free(int idx, void *ex_ptr, void *arg)
+ {
+ (void)idx;
+ (void)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)ex_ptr;
+ cJSON_AddItemToArray(gtest_ftp_json_root, ext->control_dtp_root);
+ free(ext);
+ }
+
+ static void gtest_ftp_data_dtp_exdata_free(int idx, void *ex_ptr, void *arg)
+ {
+ (void)idx;
+ (void)arg;
+ struct ftp_gtest_ext *ext = (struct ftp_gtest_ext *)ex_ptr;
+ cJSON_AddItemToArray(gtest_ftp_json_root, ext->data_dtp_root);
+ free(ext);
+ }
+
+ struct module *gtest_ftp_init(struct module_manager *mod_mgr)
+ {
+ gtest_ftp_json_root = cJSON_CreateArray();
+ struct ftp_gtest_env *env = (struct ftp_gtest_env *)calloc(1, sizeof(struct ftp_gtest_env));
+ env->mod_mgr = mod_mgr;
+
+ struct module *ftp_mod = module_manager_get_module(mod_mgr, FTP_MODULE_NAME);
+ struct ftp_decoder *ftp = module_to_ftp_decoder(ftp_mod);
+
+ ftp_subscribe(ftp,
+ gtest_ftp_on_control_request_cb,
+ gtest_ftp_on_control_response_cb,
+ gtest_ftp_on_control_dtp_cb,
+ gtest_ftp_on_data_connection_cb, env);
+
+ struct module *sess_mod = module_manager_get_module(mod_mgr, SESSION_MANAGER_MODULE_NAME);
+ struct session_manager *sess_mgr = module_to_session_manager(sess_mod);
+ env->control_request_exdata_idx = session_manager_new_session_exdata_index(sess_mgr, "FTP_GTEST_CTRL_REQ_EXDATA", gtest_ftp_ctrl_req_exdata_free, env);
+ env->control_response_exdata_idx = session_manager_new_session_exdata_index(sess_mgr, "FTP_GTEST_CTRL_RES_EXDATA", gtest_ftp_ctrl_res_exdata_free, env);
+ env->control_dtp_exdata_idx = session_manager_new_session_exdata_index(sess_mgr, "FTP_GTEST_CTRL_DTP_EXDATA", gtest_ftp_ctrl_dtp_exdata_free, env);
+ env->data_dtp_exdata_idx = session_manager_new_session_exdata_index(sess_mgr, "FTP_GTEST_DATA_DTP_EXDATA", gtest_ftp_data_dtp_exdata_free, env);
+ return module_new("GTEST_FTP_MODULE", env);
+ }
+
+ void gtest_ftp_exit(struct module_manager *mod_mgr, struct module *mod)
+ {
+ (void)mod_mgr;
+ struct ftp_gtest_env *env = (struct ftp_gtest_env *)module_get_ctx(mod);
+ free(env);
+ char *json_str = cJSON_PrintUnformatted(gtest_ftp_json_root);
+ FILE *json_fp = fopen(GTEST_RESULT_FILENAME, "w+");
+ fwrite(json_str, strlen(json_str), 1, json_fp);
+ fclose(json_fp);
+ free(json_str);
+ cJSON_Delete(gtest_ftp_json_root);
+ module_free(mod);
+ return;
+ }
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/test/decoders/ftp/ftp_module.toml b/test/decoders/ftp/ftp_module.toml
new file mode 100644
index 0000000..9e6f4e0
--- /dev/null
+++ b/test/decoders/ftp/ftp_module.toml
@@ -0,0 +1,13 @@
+#load ftp module
+[[module]]
+path = ""
+init = "ftp_init"
+exit = "ftp_exit"
+thread_init = "ftp_on_thread_init"
+thread_exit = "ftp_on_thread_exit"
+
+#load ftp gtest module
+[[module]]
+path = ""
+init = "gtest_ftp_init"
+exit = "gtest_ftp_exit"
diff --git a/test/decoders/ftp/gtest_ftp_decoder.cpp b/test/decoders/ftp/gtest_ftp_decoder.cpp
new file mode 100644
index 0000000..cbf4d5a
--- /dev/null
+++ b/test/decoders/ftp/gtest_ftp_decoder.cpp
@@ -0,0 +1,236 @@
+#include <gtest/gtest.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <arpa/inet.h>
+#include <libgen.h>
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "stellar/ftp.h"
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+#include <stellar/session.h>
+#include <stellar/exdata.h>
+#include <stellar/mq.h>
+#include <stellar/stellar.h>
+#ifdef __cplusplus
+}
+#endif
+
+TEST(FTP_GTEST, identify_by_payload)
+{
+ // true
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("USER", 4, FLOW_TYPE_C2S));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("USER test", 9, FLOW_TYPE_C2S));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220 (vsFTPd 3.0.1)", strlen("220 (vsFTPd 3.0.1)"), FLOW_TYPE_S2C));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220 (vsFTPD 3.0.2)", strlen("220 (vsFTPD 3.0.2)"), FLOW_TYPE_S2C));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220 (ftpd 3.0.3)", strlen("220 (ftpd 3.0.3)"), FLOW_TYPE_S2C));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220-Microsoft FTP Service", strlen("220-Microsoft FTP Service"), FLOW_TYPE_S2C));
+
+ // false
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("USR", 3, FLOW_TYPE_C2S));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("USRxx", 5, FLOW_TYPE_C2S));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("PASS", 4, FLOW_TYPE_C2S));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("200 OK", 6, FLOW_TYPE_S2C));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("220 (xxx 3.0.3)", strlen("220 (xxx 3.0.3)"), FLOW_TYPE_S2C));
+}
+
+TEST(FTP_GTEST, ftp_cmd_readline)
+{
+ struct ftp_interact_line line;
+ ftp_cmd_readline(&line, "USER test", strlen("USER test"));
+ ASSERT_EQ(line.cmd_refer.iov_len, 4);
+ ASSERT_EQ(line.arg_refer.iov_len, 4);
+ ASSERT_EQ(0, strncmp("USER", (char *)line.cmd_refer.iov_base, 4));
+ ASSERT_EQ(0, strncmp("test", (char *)line.arg_refer.iov_base, 4));
+
+ ftp_cmd_readline(&line, "PASS 123456\r\n", strlen("PASS 123456\r\n"));
+ ASSERT_EQ(line.cmd_refer.iov_len, 4);
+ ASSERT_EQ(line.arg_refer.iov_len, 6);
+ ASSERT_EQ(0, strncmp("PASS", (char *)line.cmd_refer.iov_base, 4));
+ ASSERT_EQ(0, strncmp("123456", (char *)line.arg_refer.iov_base, 6));
+
+ ftp_cmd_readline(&line, "CUSTOM xx1 xx2 xx3", strlen("CUSTOM xx1 xx2 xx3"));
+ ASSERT_EQ(line.cmd_refer.iov_len, 6);
+ ASSERT_EQ(line.arg_refer.iov_len, 11);
+ ASSERT_EQ(0, strncmp("CUSTOM", (char *)line.cmd_refer.iov_base, 6));
+ ASSERT_EQ(0, strncmp("xx1 xx2 xx3", (char *)line.arg_refer.iov_base, 11));
+
+ ftp_cmd_readline(&line, "LIST\n", strlen("LIST\n"));
+ ASSERT_EQ(line.cmd_refer.iov_len, 4);
+ ASSERT_TRUE(line.arg_refer.iov_base == NULL);
+ ASSERT_EQ(line.arg_refer.iov_len, 0);
+ ASSERT_EQ(0, strncmp("LIST", (char *)line.cmd_refer.iov_base, 4));
+}
+
+TEST(FTP_GTEST, ftp_skip_tail_crlf)
+{
+ ASSERT_EQ(ftp_strip_crlf("", 0), 0);
+ ASSERT_EQ(ftp_strip_crlf("\r", 1), 0);
+ ASSERT_EQ(ftp_strip_crlf("\n", 1), 0);
+ ASSERT_EQ(ftp_strip_crlf("\r\n", 2), 0);
+ ASSERT_EQ(ftp_strip_crlf("a\r\n", 3), 1);
+ ASSERT_EQ(ftp_strip_crlf("abcdefg\r\n", 9), 7);
+}
+
+TEST(FTP_GTEST, ipv4_port_style)
+{
+ int ret;
+ uint32_t ipv4_net = 0;
+ uint16_t port_net = 0;
+ fstring ip_port_style = {};
+ // normal
+ ip_port_style.iov_base = (void *)"192,168,38,2,202,95";
+ ip_port_style.iov_len = strlen("192,168,38,2,202,95");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // add tab and blank
+ ip_port_style.iov_base = (void *)" 192,168,38,2,202,95 ";
+ ip_port_style.iov_len = strlen(" 192,168,38,2,202,95 ");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // add bracket
+ ip_port_style.iov_base = (void *)"(192,168,38,2,202,95)";
+ ip_port_style.iov_len = strlen("(192,168,38,2,202,95)");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // PASV response
+ ip_port_style.iov_base = (void *)"Entering Passive Mode(192,168,38,2,202,95)";
+ ip_port_style.iov_len = strlen("Entering Passive Mode(192,168,38,2,202,95)");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // PASV response with blank
+ ip_port_style.iov_base = (void *)"Entering Passive Mode ( 192,168,38,2,202,95 )";
+ ip_port_style.iov_len = strlen("Entering Passive Mode ( 192,168,38,2,202,95 )");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+}
+
+TEST(FTP_GTEST, ipv6_port_style)
+{
+ int ret;
+ uint16_t port_net;
+ fstring ip_port_style;
+ // normal
+ ip_port_style.iov_base = (void *)"Entering Extended Passive Mode (|||12345|)";
+ ip_port_style.iov_len = strlen("Entering Extended Passive Mode (|||12345|)");
+ ret = ftp_parse_ipv6_port_style(&ip_port_style, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(port_net, htons(12345));
+
+ // add blank
+ ip_port_style.iov_base = (void *)"Entering Extended Passive Mode ( |||12345| )";
+ ip_port_style.iov_len = strlen("Entering Extended Passive Mode ( |||12345| )");
+ ret = ftp_parse_ipv6_port_style(&ip_port_style, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(port_net, htons(12345));
+}
+
+TEST(FTP_GTEST, ipv6_eprt_style)
+{
+ int ret;
+ uint16_t port_net = 0;
+ fstring eprt_style;
+ struct in6_addr ipv6addr = {};
+ // normal
+ eprt_style.iov_base = (void *)"|2|1234::abcd|12345|";
+ eprt_style.iov_len = strlen("|2|1234::abcd|12345|");
+ ret = ftp_parse_eprt_ipport_style(&eprt_style, &ipv6addr, &port_net, NULL);
+ ASSERT_EQ(ret, 0);
+ struct in6_addr tmpaddr;
+ inet_pton(AF_INET6, "1234::abcd", &tmpaddr);
+ ASSERT_EQ(0, memcmp(&tmpaddr, &ipv6addr, sizeof(struct in6_addr)));
+ ASSERT_EQ(port_net, htons(12345));
+
+ // loopback address
+ eprt_style.iov_base = (void *)"|2|::1|12345|";
+ eprt_style.iov_len = strlen("|2|::1|12345|");
+ ret = ftp_parse_eprt_ipport_style(&eprt_style, &ipv6addr, &port_net, NULL);
+ ASSERT_EQ(ret, 0);
+ inet_pton(AF_INET6, "::1", &tmpaddr);
+ ASSERT_EQ(0, memcmp(&tmpaddr, &ipv6addr, sizeof(struct in6_addr)));
+ ASSERT_EQ(port_net, htons(12345));
+
+ // add blank
+ eprt_style.iov_base = (void *)" |2|1234:4321::abcd|12345| ";
+ eprt_style.iov_len = strlen(" |2|1234:4321::abcd|12345| ");
+ ret = ftp_parse_eprt_ipport_style(&eprt_style, &ipv6addr, &port_net, NULL);
+ ASSERT_EQ(ret, 0);
+ inet_pton(AF_INET6, "1234:4321::abcd", &tmpaddr);
+ ASSERT_EQ(0, memcmp(&tmpaddr, &ipv6addr, sizeof(struct in6_addr)));
+ ASSERT_EQ(port_net, htons(12345));
+}
+
+TEST(FTP_GTEST, join_dir_file_path)
+{
+ char ab_path[PATH_MAX] = {};
+ ftp_join_absolute_path(NULL, NULL, ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "");
+
+ ftp_join_absolute_path("", "", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/");
+
+ ftp_join_absolute_path("/", "", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/");
+
+ ftp_join_absolute_path("/", ".", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/");
+
+ ftp_join_absolute_path("/", "ftp", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/ftp");
+
+ ftp_join_absolute_path("/", "./ftp", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/ftp");
+
+ ftp_join_absolute_path("/ftp", "../pub", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/pub");
+
+ ftp_join_absolute_path("/ftp/pub", "/home/foo/bar.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/home/foo/bar.txt");
+
+ ftp_join_absolute_path("/home", "test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/home/test.txt");
+
+ ftp_join_absolute_path("/home", "./test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/home/test.txt");
+
+ ftp_join_absolute_path("", "test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/test.txt");
+
+ ftp_join_absolute_path("/home", "/test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/test.txt");
+
+ ftp_join_absolute_path("/home", "../test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/test.txt");
+
+ ftp_join_absolute_path("/home/foo/bar", "../test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/home/foo/test.txt");
+
+ ftp_join_absolute_path("/home/foo/bar", "../../test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/home/test.txt");
+
+ ftp_join_absolute_path("/home/foo", "../../../../../../../test.txt", ab_path, PATH_MAX);
+ ASSERT_STREQ(ab_path, "/test.txt");
+}
+
+int main(int argc, char **argv)
+{
+ testing::InitGoogleTest(&argc, argv);
+ int ret = RUN_ALL_TESTS();
+ return ret;
+}
diff --git a/test/decoders/ftp/json/01-ftp-port-upload-download.json b/test/decoders/ftp/json/01-ftp-port-upload-download.json
new file mode 100644
index 0000000..9541844
--- /dev/null
+++ b/test/decoders/ftp/json/01-ftp-port-upload-download.json
@@ -0,0 +1,147 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "192.168.40.139:20-192.168.38.2:51808-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/ftp/2001.03147v1.pdf",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "STOR"
+ },
+ "offset": 681116,
+ "total_bytes": 681116,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.40.139:20-192.168.38.2:51809-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "offset": 76,
+ "total_bytes": 76,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.40.139:20-192.168.38.2:51810-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/pub/3cdaemon-v2r10.zip",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "RETR"
+ },
+ "offset": 953457,
+ "total_bytes": 953457,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.40.139:20-192.168.38.2:51811-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "offset": 76,
+ "total_bytes": 76,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "192.168.38.2:51805-192.168.40.139:21-6-0",
+ "0.OTHERS": "OPTS UTF8 ON",
+ "1.USER": "USER ftp",
+ "2.PASS": "PASS 111111",
+ "3.CWD": "CWD ftp",
+ "4.PORT": "PORT 192,168,38,2,202,95",
+ "5.LIST": "LIST",
+ "6.PORT": "PORT 192,168,38,2,202,96",
+ "7.STOR": "STOR 2001.03147v1.pdf",
+ "8.CWD": "CWD ../pub",
+ "9.PORT": "PORT 192,168,38,2,202,97",
+ "10.LIST": "LIST",
+ "11.PORT": "PORT 192,168,38,2,202,98",
+ "12.RETR": "RETR 3cdaemon-v2r10.zip",
+ "13.PORT": "PORT 192,168,38,2,202,99",
+ "14.LIST": "LIST",
+ "15.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_response",
+ "session": "192.168.38.2:51805-192.168.40.139:21-6-0",
+ "0.220": "220 Welcome to blah FTP service.",
+ "1.200": "200 Always in UTF8 mode.",
+ "2.OTHERS": "331 Please specify the password.",
+ "3.OTHERS": "230 Login successful.",
+ "4.OTHERS": "250 Directory successfully changed.",
+ "5.200": "200 PORT command successful. Consider using PASV.",
+ "6.OTHERS": "150 Here comes the directory listing.",
+ "7.OTHERS": "226 Directory send OK.",
+ "8.200": "200 PORT command successful. Consider using PASV.",
+ "9.OTHERS": "150 Ok to send data.",
+ "10.OTHERS": "226 Transfer complete.",
+ "11.OTHERS": "250 Directory successfully changed.",
+ "12.200": "200 PORT command successful. Consider using PASV.",
+ "13.OTHERS": "150 Here comes the directory listing.",
+ "14.OTHERS": "226 Directory send OK.",
+ "15.200": "200 PORT command successful. Consider using PASV.",
+ "16.OTHERS": "150 Opening ASCII mode data connection for 3cdaemon-v2r10.zip (949571 bytes).",
+ "17.OTHERS": "226 Transfer complete.",
+ "18.200": "200 PORT command successful. Consider using PASV.",
+ "19.OTHERS": "150 Here comes the directory listing.",
+ "20.OTHERS": "226 Directory send OK.",
+ "21.OTHERS": "221 Goodbye."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "192.168.38.2:51805-192.168.40.139:21-6-0",
+ "0.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/ftp/2001.03147v1.pdf",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "STOR"
+ },
+ "2.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "3.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/pub/3cdaemon-v2r10.zip",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "RETR"
+ },
+ "4.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/01-ftp-port-upload-download_C2S.json b/test/decoders/ftp/json/01-ftp-port-upload-download_C2S.json
new file mode 100644
index 0000000..9511c25
--- /dev/null
+++ b/test/decoders/ftp/json/01-ftp-port-upload-download_C2S.json
@@ -0,0 +1,78 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "192.168.40.139:20-192.168.38.2:51808-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/ftp/2001.03147v1.pdf",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "STOR"
+ },
+ "offset": 681116,
+ "total_bytes": 681116,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "192.168.38.2:51805-192.168.40.139:21-6-0",
+ "0.OTHERS": "OPTS UTF8 ON",
+ "1.USER": "USER ftp",
+ "2.PASS": "PASS 111111",
+ "3.CWD": "CWD ftp",
+ "4.PORT": "PORT 192,168,38,2,202,95",
+ "5.LIST": "LIST",
+ "6.PORT": "PORT 192,168,38,2,202,96",
+ "7.STOR": "STOR 2001.03147v1.pdf",
+ "8.CWD": "CWD ../pub",
+ "9.PORT": "PORT 192,168,38,2,202,97",
+ "10.LIST": "LIST",
+ "11.PORT": "PORT 192,168,38,2,202,98",
+ "12.RETR": "RETR 3cdaemon-v2r10.zip",
+ "13.PORT": "PORT 192,168,38,2,202,99",
+ "14.LIST": "LIST",
+ "15.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_dtp",
+ "session": "192.168.38.2:51805-192.168.40.139:21-6-0",
+ "0.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/ftp/2001.03147v1.pdf",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "STOR"
+ },
+ "2.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "3.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://192.168.40.139/pub/3cdaemon-v2r10.zip",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "RETR"
+ },
+ "4.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/01-ftp-port-upload-download_S2C.json b/test/decoders/ftp/json/01-ftp-port-upload-download_S2C.json
new file mode 100644
index 0000000..83a2a7e
--- /dev/null
+++ b/test/decoders/ftp/json/01-ftp-port-upload-download_S2C.json
@@ -0,0 +1,28 @@
+[
+ {
+ "topic": "control_response",
+ "session": "192.168.38.2:51805-192.168.40.139:21-6-0",
+ "0.220": "220 Welcome to blah FTP service.",
+ "1.200": "200 Always in UTF8 mode.",
+ "2.OTHERS": "331 Please specify the password.",
+ "3.OTHERS": "230 Login successful.",
+ "4.OTHERS": "250 Directory successfully changed.",
+ "5.200": "200 PORT command successful. Consider using PASV.",
+ "6.OTHERS": "150 Here comes the directory listing.",
+ "7.OTHERS": "226 Directory send OK.",
+ "8.200": "200 PORT command successful. Consider using PASV.",
+ "9.OTHERS": "150 Ok to send data.",
+ "10.OTHERS": "226 Transfer complete.",
+ "11.OTHERS": "250 Directory successfully changed.",
+ "12.200": "200 PORT command successful. Consider using PASV.",
+ "13.OTHERS": "150 Here comes the directory listing.",
+ "14.OTHERS": "226 Directory send OK.",
+ "15.200": "200 PORT command successful. Consider using PASV.",
+ "16.OTHERS": "150 Opening ASCII mode data connection for 3cdaemon-v2r10.zip (949571 bytes).",
+ "17.OTHERS": "226 Transfer complete.",
+ "18.200": "200 PORT command successful. Consider using PASV.",
+ "19.OTHERS": "150 Here comes the directory listing.",
+ "20.OTHERS": "226 Directory send OK.",
+ "21.OTHERS": "221 Goodbye."
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/02-ftp_v6_1.json b/test/decoders/ftp/json/02-ftp_v6_1.json
new file mode 100644
index 0000000..7ea06d3
--- /dev/null
+++ b/test/decoders/ftp/json/02-ftp_v6_1.json
@@ -0,0 +1,160 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "::1:39002-::1:10791-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 65,
+ "total_bytes": 65,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:41426-::1:20987-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/123.txt",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "offset": 44,
+ "total_bytes": 44,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:59846-::1:50928-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/test.txt",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ },
+ "offset": 140,
+ "total_bytes": 140,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:50872-::1:14779-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 131,
+ "total_bytes": 131,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "::1:47756-::1:21-6-0",
+ "0.OTHERS": "FEAT",
+ "1.OTHERS": "AUTH TLS",
+ "2.OTHERS": "OPTS UTF8 ON",
+ "3.USER": "USER ftp",
+ "4.PASS": "PASS 111111",
+ "5.OTHERS": "PWD",
+ "6.CWD": "CWD /pub",
+ "7.EPSV": "EPSV",
+ "8.LIST": "LIST",
+ "9.OTHERS": "TYPE I",
+ "10.OTHERS": "SIZE 123.txt",
+ "11.OTHERS": "MDTM 123.txt",
+ "12.EPSV": "EPSV",
+ "13.RETR": "RETR 123.txt",
+ "14.EPSV": "EPSV",
+ "15.OTHERS": "ALLO 140",
+ "16.STOR": "STOR test.txt",
+ "17.OTHERS": "SITE UTIME 20240721024406 test.txt",
+ "18.OTHERS": "TYPE A",
+ "19.EPSV": "EPSV",
+ "20.LIST": "LIST",
+ "21.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_response",
+ "session": "::1:47756-::1:21-6-0",
+ "0.220": "220 Welcome to blah FTP service.",
+ "1.OTHERS": "211-Features:",
+ "2.OTHERS": " EPRT",
+ "3.OTHERS": " EPSV",
+ "4.OTHERS": " MDTM",
+ "5.OTHERS": " PASV",
+ "6.OTHERS": " REST STREAM",
+ "7.OTHERS": " SIZE",
+ "8.OTHERS": " TVFS",
+ "9.OTHERS": " UTF8",
+ "10.OTHERS": "211 End",
+ "11.OTHERS": "530 Please login with USER and PASS.",
+ "12.200": "200 Always in UTF8 mode.",
+ "13.OTHERS": "331 Please specify the password.",
+ "14.OTHERS": "230 Login successful.",
+ "15.OTHERS": "257 \"/\" is the current directory",
+ "16.OTHERS": "250 Directory successfully changed.",
+ "17.229": "229 Entering Extended Passive Mode (|||10791|)",
+ "18.OTHERS": "150 Here comes the directory listing.",
+ "19.OTHERS": "226 Directory send OK.",
+ "20.200": "200 Switching to Binary mode.",
+ "21.OTHERS": "213 44",
+ "22.OTHERS": "213 20240721024145",
+ "23.229": "229 Entering Extended Passive Mode (|||20987|)",
+ "24.OTHERS": "150 Opening BINARY mode data connection for 123.txt (44 bytes).",
+ "25.OTHERS": "226 Transfer complete.",
+ "26.229": "229 Entering Extended Passive Mode (|||50928|)",
+ "27.OTHERS": "202 ALLO command ignored.",
+ "28.OTHERS": "150 Ok to send data.",
+ "29.OTHERS": "226 Transfer complete.",
+ "30.OTHERS": "550 Permission denied.",
+ "31.200": "200 Switching to ASCII mode.",
+ "32.229": "229 Entering Extended Passive Mode (|||14779|)",
+ "33.OTHERS": "150 Here comes the directory listing.",
+ "34.OTHERS": "226 Directory send OK.",
+ "35.OTHERS": "221 Goodbye."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "::1:47756-::1:21-6-0",
+ "0.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/123.txt",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "2.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/test.txt",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ },
+ "3.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "LIST"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/03-ipv6_eport_upload_download.json b/test/decoders/ftp/json/03-ipv6_eport_upload_download.json
new file mode 100644
index 0000000..db35746
--- /dev/null
+++ b/test/decoders/ftp/json/03-ipv6_eport_upload_download.json
@@ -0,0 +1,167 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "::1:20-::1:49998-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "offset": 65,
+ "total_bytes": 65,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:20-::1:40784-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/123.txt",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "RETR"
+ },
+ "offset": 44,
+ "total_bytes": 44,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:20-::1:32865-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "offset": 65,
+ "total_bytes": 65,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:20-::1:55267-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/test.txt",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "STOR"
+ },
+ "offset": 140,
+ "total_bytes": 140,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "::1:20-::1:34844-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "offset": 131,
+ "total_bytes": 131,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "::1:49594-::1:21-6-0",
+ "0.USER": "USER ftp",
+ "1.PASS": "PASS 111111",
+ "2.SYST": "SYST",
+ "3.CWD": "CWD pub",
+ "4.EPRT": "EPRT |2|::1|49998|",
+ "5.LIST": "LIST",
+ "6.OTHERS": "TYPE I",
+ "7.EPRT": "EPRT |2|::1|40784|",
+ "8.RETR": "RETR 123.txt",
+ "9.OTHERS": "TYPE A",
+ "10.EPRT": "EPRT |2|::1|32865|",
+ "11.LIST": "LIST",
+ "12.OTHERS": "TYPE I",
+ "13.EPRT": "EPRT |2|::1|55267|",
+ "14.STOR": "STOR test.txt",
+ "15.OTHERS": "TYPE A",
+ "16.EPRT": "EPRT |2|::1|34844|",
+ "17.LIST": "LIST",
+ "18.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_response",
+ "session": "::1:49594-::1:21-6-0",
+ "0.220": "220 Welcome to blah FTP service.",
+ "1.OTHERS": "331 Please specify the password.",
+ "2.OTHERS": "230 Login successful.",
+ "3.OTHERS": "215 UNIX Type: L8",
+ "4.OTHERS": "250 Directory successfully changed.",
+ "5.200": "200 EPRT command successful. Consider using EPSV.",
+ "6.OTHERS": "150 Here comes the directory listing.",
+ "7.OTHERS": "226 Directory send OK.",
+ "8.200": "200 Switching to Binary mode.",
+ "9.200": "200 EPRT command successful. Consider using EPSV.",
+ "10.OTHERS": "150 Opening BINARY mode data connection for 123.txt (44 bytes).",
+ "11.OTHERS": "226 Transfer complete.",
+ "12.200": "200 Switching to ASCII mode.",
+ "13.200": "200 EPRT command successful. Consider using EPSV.",
+ "14.OTHERS": "150 Here comes the directory listing.",
+ "15.OTHERS": "226 Directory send OK.",
+ "16.200": "200 Switching to Binary mode.",
+ "17.200": "200 EPRT command successful. Consider using EPSV.",
+ "18.OTHERS": "150 Ok to send data.",
+ "19.OTHERS": "226 Transfer complete.",
+ "20.200": "200 Switching to ASCII mode.",
+ "21.200": "200 EPRT command successful. Consider using EPSV.",
+ "22.OTHERS": "150 Here comes the directory listing.",
+ "23.OTHERS": "226 Directory send OK.",
+ "24.OTHERS": "221 Goodbye."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "::1:49594-::1:21-6-0",
+ "0.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/123.txt",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "RETR"
+ },
+ "2.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "port",
+ "cmd": "LIST"
+ },
+ "3.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://::1/pub/test.txt",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "STOR"
+ },
+ "4.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "stor",
+ "mode": "port",
+ "cmd": "LIST"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/04-ftp-banner-no-ftp-characters.json b/test/decoders/ftp/json/04-ftp-banner-no-ftp-characters.json
new file mode 100644
index 0000000..ef8cda1
--- /dev/null
+++ b/test/decoders/ftp/json/04-ftp-banner-no-ftp-characters.json
@@ -0,0 +1,54 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "10.20.144.150:35976-10.20.144.151:16013-6-0",
+ "dtp": {
+ "username": "cdts3500",
+ "password": "cdts3500",
+ "uri": "ftp://10.20.144.151/qgpl/apkeyf.apkeyf",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "offset": 439,
+ "total_bytes": 439,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "10.20.144.150:35974-10.20.144.151:21-6-0",
+ "0.USER": "USER cdts3500",
+ "1.PASS": "PASS cdts3500",
+ "2.SYST": "SYST",
+ "3.OTHERS": "SITE NAMEFMT",
+ "4.OTHERS": "PWD",
+ "5.PASV": "PASV",
+ "6.RETR": "RETR qgpl/apkeyf.apkeyf",
+ "7.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_response",
+ "session": "10.20.144.150:35974-10.20.144.151:21-6-0",
+ "0.OTHERS": "331 Enter password.",
+ "1.OTHERS": "230 CDTS3500 logged on.",
+ "2.OTHERS": "215 OS/400 is the remote operating system. The TCP/IP version is \"V5R2M0\".",
+ "3.OTHERS": "250 Now using naming format \"0\".",
+ "4.OTHERS": "257 \"CDTS3500\" is current library.",
+ "5.227": "227 Entering Passive Mode (10,20,144,151,62,141).",
+ "6.OTHERS": "150 Retrieving member APKEYF in file APKEYF in library QGPL.",
+ "7.OTHERS": "250 File transfer completed successfully.",
+ "8.OTHERS": "221 QUIT subcommand received."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "10.20.144.150:35974-10.20.144.151:21-6-0",
+ "0.dtp": {
+ "username": "cdts3500",
+ "password": "cdts3500",
+ "uri": "ftp://10.20.144.151/qgpl/apkeyf.apkeyf",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/05-only-ctrl-link.json b/test/decoders/ftp/json/05-only-ctrl-link.json
new file mode 100644
index 0000000..036bf70
--- /dev/null
+++ b/test/decoders/ftp/json/05-only-ctrl-link.json
@@ -0,0 +1,33 @@
+[
+ {
+ "topic": "control_request",
+ "session": "192.168.20.1:40920-192.168.60.1:21-6-0",
+ "0.USER": "USER test1",
+ "1.PASS": "PASS iiecas2021",
+ "2.OTHERS": "TYPE I",
+ "3.PASV": "PASV",
+ "4.STOR": "STOR /home/test1/20210622201359人肉炸弹.txt"
+ },
+ {
+ "topic": "control_response",
+ "session": "192.168.20.1:40920-192.168.60.1:21-6-0",
+ "0.220": "220 (vsFTPd 3.0.2)",
+ "1.OTHERS": "331 Please specify the password.",
+ "2.OTHERS": "230 Login successful.",
+ "3.200": "200 Switching to Binary mode.",
+ "4.227": "227 Entering Passive Mode (192,168,60,1,39,16).",
+ "5.OTHERS": "150 Ok to send data."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "192.168.20.1:40920-192.168.60.1:21-6-0",
+ "0.dtp": {
+ "username": "test1",
+ "password": "iiecas2021",
+ "uri": "ftp://192.168.60.1/home/test1/20210622201359人肉炸弹.txt",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/06-ftp_pasv-upload-download.json b/test/decoders/ftp/json/06-ftp_pasv-upload-download.json
new file mode 100644
index 0000000..9a383e4
--- /dev/null
+++ b/test/decoders/ftp/json/06-ftp_pasv-upload-download.json
@@ -0,0 +1,135 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:59814-127.0.0.1:13144-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 61,
+ "total_bytes": 61,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:38754-127.0.0.1:15810-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 131,
+ "total_bytes": 131,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:52584-127.0.0.1:6554-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://127.0.0.1/pub/123.txt",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "offset": 44,
+ "total_bytes": 44,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:32799-127.0.0.1:58439-6-0",
+ "dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://127.0.0.1/pub/test.txt",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ },
+ "offset": 140,
+ "total_bytes": 140,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "127.0.0.1:42042-127.0.0.1:21-6-0",
+ "0.USER": "USER ftp",
+ "1.PASS": "PASS 111111",
+ "2.SYST": "SYST",
+ "3.PASV": "PASV",
+ "4.LIST": "LIST",
+ "5.CWD": "CWD pub",
+ "6.PASV": "PASV",
+ "7.LIST": "LIST",
+ "8.OTHERS": "TYPE I",
+ "9.PASV": "PASV",
+ "10.RETR": "RETR 123.txt",
+ "11.PASV": "PASV",
+ "12.STOR": "STOR test.txt",
+ "13.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_response",
+ "session": "127.0.0.1:42042-127.0.0.1:21-6-0",
+ "0.220": "220 Welcome to blah FTP service.",
+ "1.OTHERS": "331 Please specify the password.",
+ "2.OTHERS": "230 Login successful.",
+ "3.OTHERS": "215 UNIX Type: L8",
+ "4.227": "227 Entering Passive Mode (127,0,0,1,51,88).",
+ "5.OTHERS": "150 Here comes the directory listing.",
+ "6.OTHERS": "226 Directory send OK.",
+ "7.OTHERS": "250 Directory successfully changed.",
+ "8.227": "227 Entering Passive Mode (127,0,0,1,61,194).",
+ "9.OTHERS": "150 Here comes the directory listing.",
+ "10.OTHERS": "226 Directory send OK.",
+ "11.200": "200 Switching to Binary mode.",
+ "12.227": "227 Entering Passive Mode (127,0,0,1,25,154).",
+ "13.OTHERS": "150 Opening BINARY mode data connection for 123.txt (44 bytes).",
+ "14.OTHERS": "226 Transfer complete.",
+ "15.227": "227 Entering Passive Mode (127,0,0,1,228,71).",
+ "16.OTHERS": "150 Ok to send data.",
+ "17.OTHERS": "226 Transfer complete.",
+ "18.OTHERS": "221 Goodbye."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "127.0.0.1:42042-127.0.0.1:21-6-0",
+ "0.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "2.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://127.0.0.1/pub/123.txt",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "3.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://127.0.0.1/pub/test.txt",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/06-ftp_pasv-upload-download_C2S.json b/test/decoders/ftp/json/06-ftp_pasv-upload-download_C2S.json
new file mode 100644
index 0000000..efd0e2b
--- /dev/null
+++ b/test/decoders/ftp/json/06-ftp_pasv-upload-download_C2S.json
@@ -0,0 +1,54 @@
+[
+ {
+ "topic": "control_request",
+ "session": "127.0.0.1:42042-127.0.0.1:21-6-0",
+ "0.USER": "USER ftp",
+ "1.PASS": "PASS 111111",
+ "2.SYST": "SYST",
+ "3.PASV": "PASV",
+ "4.LIST": "LIST",
+ "5.CWD": "CWD pub",
+ "6.PASV": "PASV",
+ "7.LIST": "LIST",
+ "8.OTHERS": "TYPE I",
+ "9.PASV": "PASV",
+ "10.RETR": "RETR 123.txt",
+ "11.PASV": "PASV",
+ "12.STOR": "STOR test.txt",
+ "13.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_dtp",
+ "session": "127.0.0.1:42042-127.0.0.1:21-6-0",
+ "0.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "2.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://127.0.0.1/pub/123.txt",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "3.dtp": {
+ "username": "ftp",
+ "password": "111111",
+ "uri": "ftp://127.0.0.1/pub/test.txt",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/06-ftp_pasv-upload-download_S2C.json b/test/decoders/ftp/json/06-ftp_pasv-upload-download_S2C.json
new file mode 100644
index 0000000..a0d28ef
--- /dev/null
+++ b/test/decoders/ftp/json/06-ftp_pasv-upload-download_S2C.json
@@ -0,0 +1,46 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:59814-127.0.0.1:13144-6-0",
+ "offset": 61,
+ "total_bytes": 61,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:38754-127.0.0.1:15810-6-0",
+ "offset": 131,
+ "total_bytes": 131,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "127.0.0.1:52584-127.0.0.1:6554-6-0",
+ "offset": 44,
+ "total_bytes": 44,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_response",
+ "session": "127.0.0.1:42042-127.0.0.1:21-6-0",
+ "0.220": "220 Welcome to blah FTP service.",
+ "1.OTHERS": "331 Please specify the password.",
+ "2.OTHERS": "230 Login successful.",
+ "3.OTHERS": "215 UNIX Type: L8",
+ "4.227": "227 Entering Passive Mode (127,0,0,1,51,88).",
+ "5.OTHERS": "150 Here comes the directory listing.",
+ "6.OTHERS": "226 Directory send OK.",
+ "7.OTHERS": "250 Directory successfully changed.",
+ "8.227": "227 Entering Passive Mode (127,0,0,1,61,194).",
+ "9.OTHERS": "150 Here comes the directory listing.",
+ "10.OTHERS": "226 Directory send OK.",
+ "11.200": "200 Switching to Binary mode.",
+ "12.227": "227 Entering Passive Mode (127,0,0,1,25,154).",
+ "13.OTHERS": "150 Opening BINARY mode data connection for 123.txt (44 bytes).",
+ "14.OTHERS": "226 Transfer complete.",
+ "15.227": "227 Entering Passive Mode (127,0,0,1,228,71).",
+ "16.OTHERS": "150 Ok to send data.",
+ "17.OTHERS": "226 Transfer complete.",
+ "18.OTHERS": "221 Goodbye."
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/07-ftp-start-with-syst-command.json b/test/decoders/ftp/json/07-ftp-start-with-syst-command.json
new file mode 100644
index 0000000..08755d1
--- /dev/null
+++ b/test/decoders/ftp/json/07-ftp-start-with-syst-command.json
@@ -0,0 +1,37 @@
+[
+ {
+ "topic": "control_request",
+ "session": "10.100.237.39:48406-172.2.1.200:21-6-0",
+ "0.SYST": "SYST",
+ "1.USER": "USER administrator",
+ "2.PASS": "PASS admin@123",
+ "3.OTHERS": "TYPE I",
+ "4.PASV": "PASV",
+ "5.RETR": "RETR JS_XuZhou_1_1_App4",
+ "6.OTHERS": "QUIT"
+ },
+ {
+ "topic": "control_response",
+ "session": "10.100.237.39:48406-172.2.1.200:21-6-0",
+ "0.220": "220 Microsoft FTP Service",
+ "1.OTHERS": "215 Windows_NT",
+ "2.OTHERS": "331 Password required for administrator.",
+ "3.OTHERS": "230 User logged in.",
+ "4.200": "200 Type set to I.",
+ "5.227": "227 Entering Passive Mode (172,2,1,200,250,142).",
+ "6.OTHERS": "550 The system cannot find the file specified. ",
+ "7.OTHERS": "221 Goodbye."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "10.100.237.39:48406-172.2.1.200:21-6-0",
+ "0.dtp": {
+ "username": "administrator",
+ "password": "admin@123",
+ "uri": "ftp://172.2.1.200/JS_XuZhou_1_1_App4",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/08-ftp_EPSV_229_ipv4.json b/test/decoders/ftp/json/08-ftp_EPSV_229_ipv4.json
new file mode 100644
index 0000000..1556318
--- /dev/null
+++ b/test/decoders/ftp/json/08-ftp_EPSV_229_ipv4.json
@@ -0,0 +1,34 @@
+[
+ {
+ "topic": "control_request",
+ "session": "10.128.22.236:56976-172.17.0.74:21-6-0",
+ "0.USER": "USER YaaoFtpAdmin",
+ "1.PASS": "PASS JSyaao@2008",
+ "2.OTHERS": "TYPE I",
+ "3.EPSV": "EPSV",
+ "4.STOR": "STOR 32011143800147_20220503_134754_01.jpg"
+ },
+ {
+ "topic": "control_response",
+ "session": "10.128.22.236:56976-172.17.0.74:21-6-0",
+ "0.220": "220 Microsoft FTP Service",
+ "1.OTHERS": "331 Password required",
+ "2.OTHERS": "230 User logged in.",
+ "3.200": "200 Type set to I.",
+ "4.229": "229 Entering Extended Passive Mode (|||50929|)",
+ "5.OTHERS": "125 Data connection already open; Transfer starting.",
+ "6.OTHERS": "550 There is not enough space on the disk. "
+ },
+ {
+ "topic": "control_dtp",
+ "session": "10.128.22.236:56976-172.17.0.74:21-6-0",
+ "0.dtp": {
+ "username": "YaaoFtpAdmin",
+ "password": "JSyaao@2008",
+ "uri": "ftp://172.17.0.74/32011143800147_20220503_134754_01.jpg",
+ "dir": "stor",
+ "mode": "pasv",
+ "cmd": "STOR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/10-ftp-sdedu.json b/test/decoders/ftp/json/10-ftp-sdedu.json
new file mode 100644
index 0000000..bf41792
--- /dev/null
+++ b/test/decoders/ftp/json/10-ftp-sdedu.json
@@ -0,0 +1,101 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:63221-218.13.32.6:20008-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 1247,
+ "total_bytes": 1247,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:63234-218.13.32.6:20009-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "uri": "ftp://218.13.32.6/temp/�ĵ�1.docx",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "offset": 33598,
+ "total_bytes": 33598,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "192.168.32.27:63219-218.13.32.6:21-6-0",
+ "0.USER": "USER anonymous",
+ "1.PASS": "PASS IEUser@",
+ "2.OTHERS": "opts utf8 on",
+ "3.OTHERS": "PWD",
+ "4.CWD": "CWD /temp/",
+ "5.OTHERS": "TYPE A",
+ "6.PASV": "PASV",
+ "7.LIST": "LIST",
+ "8.OTHERS": "noop",
+ "9.CWD": "CWD /temp/",
+ "10.OTHERS": "noop",
+ "11.CWD": "CWD /temp/",
+ "12.OTHERS": "PWD",
+ "13.CWD": "CWD /temp/",
+ "14.OTHERS": "TYPE I",
+ "15.PASV": "PASV",
+ "16.OTHERS": "SIZE �ĵ�1.docx",
+ "17.RETR": "RETR �ĵ�1.docx",
+ "18.CWD": "CWD /temp/"
+ },
+ {
+ "topic": "control_response",
+ "session": "192.168.32.27:63219-218.13.32.6:21-6-0",
+ "0.220": "220-Microsoft FTP Service",
+ "1.220": "220 FTP Server",
+ "2.OTHERS": "331 Anonymous access allowed, send identity (e-mail name) as password.",
+ "3.OTHERS": "230-Welcome!",
+ "4.OTHERS": "230 Anonymous user logged in.",
+ "5.OTHERS": "501 option not supported",
+ "6.OTHERS": "257 \"/\" is current directory.",
+ "7.OTHERS": "250 CWD command successful.",
+ "8.200": "200 Type set to A.",
+ "9.227": "227 Entering Passive Mode (218,13,32,6,78,40).",
+ "10.OTHERS": "125 Data connection already open; Transfer starting.",
+ "11.OTHERS": "226 Transfer complete.",
+ "12.200": "200 NOOP command successful.",
+ "13.OTHERS": "250 CWD command successful.",
+ "14.200": "200 NOOP command successful.",
+ "15.OTHERS": "250 CWD command successful.",
+ "16.OTHERS": "257 \"/temp\" is current directory.",
+ "17.OTHERS": "250 CWD command successful.",
+ "18.200": "200 Type set to I.",
+ "19.227": "227 Entering Passive Mode (218,13,32,6,78,41).",
+ "20.OTHERS": "213 33598",
+ "21.OTHERS": "125 Data connection already open; Transfer starting.",
+ "22.OTHERS": "226 Transfer complete.",
+ "23.OTHERS": "250 CWD command successful."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "192.168.32.27:63219-218.13.32.6:21-6-0",
+ "0.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "uri": "ftp://218.13.32.6/temp/�ĵ�1.docx",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/11-ftp-sjtu.json b/test/decoders/ftp/json/11-ftp-sjtu.json
new file mode 100644
index 0000000..cdf8696
--- /dev/null
+++ b/test/decoders/ftp/json/11-ftp-sjtu.json
@@ -0,0 +1,354 @@
+[
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:64300-202.38.97.230:14978-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 2644,
+ "total_bytes": 2644,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:64308-202.38.97.230:64553-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "uri": "ftp://202.38.97.230/sjtu.edu.cn.html",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "offset": 333,
+ "total_bytes": 333,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:64312-202.38.97.230:58085-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 784,
+ "total_bytes": 784,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:64317-202.38.97.230:46331-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 455,
+ "total_bytes": 455,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:64321-202.38.97.230:25603-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "offset": 80,
+ "total_bytes": 80,
+ "is_finished": 1
+ },
+ {
+ "topic": "data_dtp",
+ "session": "192.168.32.27:64333-202.38.97.230:56773-6-0",
+ "dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "uri": "ftp://202.38.97.230/pub/software/chinese/zhcon-0.2.3-1.i386.rpm",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "offset": 5079386,
+ "total_bytes": 5079386,
+ "is_finished": 1
+ },
+ {
+ "topic": "control_request",
+ "session": "192.168.32.27:64299-202.38.97.230:21-6-0",
+ "0.USER": "USER anonymous",
+ "1.PASS": "PASS IEUser@",
+ "2.OTHERS": "opts utf8 on",
+ "3.OTHERS": "syst",
+ "4.OTHERS": "site help",
+ "5.OTHERS": "PWD",
+ "6.OTHERS": "TYPE A",
+ "7.PASV": "PASV",
+ "8.LIST": "LIST",
+ "9.OTHERS": "PWD",
+ "10.CWD": "CWD Linux-HOWTO",
+ "11.CWD": "CWD /",
+ "12.CWD": "CWD archlinux",
+ "13.CWD": "CWD /",
+ "14.CWD": "CWD centos",
+ "15.CWD": "CWD /",
+ "16.CWD": "CWD csw",
+ "17.CWD": "CWD /",
+ "18.CWD": "CWD debian",
+ "19.CWD": "CWD /",
+ "20.CWD": "CWD debian-cd",
+ "21.CWD": "CWD /",
+ "22.CWD": "CWD debian-security",
+ "23.CWD": "CWD /",
+ "24.CWD": "CWD deepin",
+ "25.CWD": "CWD /",
+ "26.CWD": "CWD deepin-cd",
+ "27.CWD": "CWD /",
+ "28.CWD": "CWD docker-ce",
+ "29.CWD": "CWD /",
+ "30.CWD": "CWD fedora",
+ "31.CWD": "CWD /",
+ "32.CWD": "CWD linuxmint",
+ "33.CWD": "CWD /",
+ "34.CWD": "CWD linuxmint-cd",
+ "35.CWD": "CWD /",
+ "36.CWD": "CWD mageia",
+ "37.CWD": "CWD /",
+ "38.CWD": "CWD opensuse",
+ "39.CWD": "CWD /",
+ "40.CWD": "CWD openvz",
+ "41.CWD": "CWD /",
+ "42.CWD": "CWD remi",
+ "43.CWD": "CWD /",
+ "44.CWD": "CWD scientific",
+ "45.CWD": "CWD /",
+ "46.CWD": "CWD ubuntu",
+ "47.CWD": "CWD /",
+ "48.CWD": "CWD ubuntu-cd",
+ "49.CWD": "CWD /",
+ "50.OTHERS": "noop",
+ "51.CWD": "CWD /",
+ "52.OTHERS": "noop",
+ "53.CWD": "CWD /",
+ "54.OTHERS": "PWD",
+ "55.CWD": "CWD /",
+ "56.OTHERS": "TYPE I",
+ "57.PASV": "PASV",
+ "58.OTHERS": "SIZE sjtu.edu.cn.html",
+ "59.RETR": "RETR sjtu.edu.cn.html",
+ "60.OTHERS": "noop",
+ "61.CWD": "CWD /pub/",
+ "62.OTHERS": "TYPE A",
+ "63.PASV": "PASV",
+ "64.LIST": "LIST",
+ "65.OTHERS": "PWD",
+ "66.CWD": "CWD CPAN",
+ "67.CWD": "CWD /pub",
+ "68.CWD": "CWD LDP",
+ "69.CWD": "CWD /pub",
+ "70.CWD": "CWD fedora",
+ "71.CWD": "CWD /pub",
+ "72.CWD": "CWD internet-drafts",
+ "73.CWD": "CWD /pub",
+ "74.CWD": "CWD rfc",
+ "75.CWD": "CWD /pub",
+ "76.OTHERS": "noop",
+ "77.CWD": "CWD /pub/software/",
+ "78.OTHERS": "TYPE A",
+ "79.PASV": "PASV",
+ "80.LIST": "LIST",
+ "81.OTHERS": "noop",
+ "82.CWD": "CWD /pub/software/chinese/",
+ "83.OTHERS": "TYPE A",
+ "84.PASV": "PASV",
+ "85.LIST": "LIST",
+ "86.OTHERS": "noop",
+ "87.CWD": "CWD /pub/software/chinese/",
+ "88.OTHERS": "noop",
+ "89.CWD": "CWD /pub/software/chinese/",
+ "90.OTHERS": "PWD",
+ "91.CWD": "CWD /pub/software/chinese/",
+ "92.OTHERS": "TYPE I",
+ "93.PASV": "PASV",
+ "94.OTHERS": "SIZE zhcon-0.2.3-1.i386.rpm",
+ "95.RETR": "RETR zhcon-0.2.3-1.i386.rpm",
+ "96.CWD": "CWD /pub/software/chinese/"
+ },
+ {
+ "topic": "control_response",
+ "session": "192.168.32.27:64299-202.38.97.230:21-6-0",
+ "0.220": "220 (vsFTPd 3.0.2)",
+ "1.OTHERS": "331 Please specify the password.",
+ "2.OTHERS": "230 Login successful.",
+ "3.200": "200 Always in UTF8 mode.",
+ "4.OTHERS": "215 UNIX Type: L8",
+ "5.OTHERS": "550 Permission denied.",
+ "6.OTHERS": "257 \"/\"",
+ "7.200": "200 Switching to ASCII mode.",
+ "8.227": "227 Entering Passive Mode (202,38,97,230,58,130).",
+ "9.OTHERS": "150 Here comes the directory listing.",
+ "10.OTHERS": "226 Directory send OK.",
+ "11.OTHERS": "257 \"/\"",
+ "12.OTHERS": "250 Directory successfully changed.",
+ "13.OTHERS": "250 Directory successfully changed.",
+ "14.OTHERS": "250 Directory successfully changed.",
+ "15.OTHERS": "250 Directory successfully changed.",
+ "16.OTHERS": "250 Directory successfully changed.",
+ "17.OTHERS": "250 Directory successfully changed.",
+ "18.OTHERS": "250 Directory successfully changed.",
+ "19.OTHERS": "250 Directory successfully changed.",
+ "20.OTHERS": "250 Directory successfully changed.",
+ "21.OTHERS": "250 Directory successfully changed.",
+ "22.OTHERS": "250 Directory successfully changed.",
+ "23.OTHERS": "250 Directory successfully changed.",
+ "24.OTHERS": "250 Directory successfully changed.",
+ "25.OTHERS": "250 Directory successfully changed.",
+ "26.OTHERS": "250 Directory successfully changed.",
+ "27.OTHERS": "250 Directory successfully changed.",
+ "28.OTHERS": "250 Directory successfully changed.",
+ "29.OTHERS": "250 Directory successfully changed.",
+ "30.OTHERS": "250 Directory successfully changed.",
+ "31.OTHERS": "250 Directory successfully changed.",
+ "32.OTHERS": "250 Directory successfully changed.",
+ "33.OTHERS": "250 Directory successfully changed.",
+ "34.OTHERS": "250 Directory successfully changed.",
+ "35.OTHERS": "250 Directory successfully changed.",
+ "36.OTHERS": "250 Directory successfully changed.",
+ "37.OTHERS": "250 Directory successfully changed.",
+ "38.OTHERS": "250 Directory successfully changed.",
+ "39.OTHERS": "250 Directory successfully changed.",
+ "40.OTHERS": "250 Directory successfully changed.",
+ "41.OTHERS": "250 Directory successfully changed.",
+ "42.OTHERS": "250-",
+ "43.OTHERS": "250- OpenVZ consists of a kernel, user-level tools and templates.",
+ "44.OTHERS": "250- Kernel and tools are needed to install OpenVZ, and templates are",
+ "45.OTHERS": "250- needed to create containers.",
+ "46.OTHERS": "250- installation. For information about templates, read [4]this document.",
+ "47.OTHERS": "250- Yum repository",
+ "48.OTHERS": "250- 4. http://wiki.openvz.org/OS_template_cache_preparation",
+ "49.OTHERS": "250 Directory successfully changed.",
+ "50.OTHERS": "250 Directory successfully changed.",
+ "51.OTHERS": "250 Directory successfully changed.",
+ "52.OTHERS": "250 Directory successfully changed.",
+ "53.OTHERS": "250 Directory successfully changed.",
+ "54.OTHERS": "250 Directory successfully changed.",
+ "55.OTHERS": "250 Directory successfully changed.",
+ "56.OTHERS": "250 Directory successfully changed.",
+ "57.OTHERS": "250 Directory successfully changed.",
+ "58.200": "200 NOOP ok.",
+ "59.OTHERS": "250 Directory successfully changed.",
+ "60.200": "200 NOOP ok.",
+ "61.OTHERS": "250 Directory successfully changed.",
+ "62.OTHERS": "257 \"/\"",
+ "63.OTHERS": "250 Directory successfully changed.",
+ "64.200": "200 Switching to Binary mode.",
+ "65.227": "227 Entering Passive Mode (202,38,97,230,252,41).",
+ "66.OTHERS": "213 333",
+ "67.OTHERS": "150 Opening BINARY mode data connection for sjtu.edu.cn.html (333 bytes).",
+ "68.OTHERS": "226 Transfer complete.",
+ "69.200": "200 NOOP ok.",
+ "70.OTHERS": "250 Directory successfully changed.",
+ "71.200": "200 Switching to ASCII mode.",
+ "72.227": "227 Entering Passive Mode (202,38,97,230,226,229).",
+ "73.OTHERS": "150 Here comes the directory listing.",
+ "74.OTHERS": "226 Directory send OK.",
+ "75.OTHERS": "257 \"/pub\"",
+ "76.OTHERS": "250-The Comprehensive Perl Archive Network (http://www.cpan.org/)",
+ "77.OTHERS": "250-master site has been from the very beginning (1995) hosted at FUNET,",
+ "78.OTHERS": "250-the Finnish University NETwork.",
+ "79.OTHERS": "250-",
+ "80.OTHERS": "250 Directory successfully changed.",
+ "81.OTHERS": "250 Directory successfully changed.",
+ "82.OTHERS": "250 Directory successfully changed.",
+ "83.OTHERS": "250 Directory successfully changed.",
+ "84.OTHERS": "250 Directory successfully changed.",
+ "85.OTHERS": "250 Directory successfully changed.",
+ "86.OTHERS": "250 Directory successfully changed.",
+ "87.OTHERS": "250 Directory successfully changed.",
+ "88.OTHERS": "250 Directory successfully changed.",
+ "89.200": "200 NOOP ok.",
+ "90.OTHERS": "250 Directory successfully changed.",
+ "91.200": "200 Switching to ASCII mode.",
+ "92.227": "227 Entering Passive Mode (202,38,97,230,180,251).",
+ "93.OTHERS": "150 Here comes the directory listing.",
+ "94.OTHERS": "226 Directory send OK.",
+ "95.200": "200 NOOP ok.",
+ "96.OTHERS": "250 Directory successfully changed.",
+ "97.200": "200 Switching to ASCII mode.",
+ "98.227": "227 Entering Passive Mode (202,38,97,230,100,3).",
+ "99.OTHERS": "150 Here comes the directory listing.",
+ "100.OTHERS": "226 Directory send OK.",
+ "101.200": "200 NOOP ok.",
+ "102.OTHERS": "250 Directory successfully changed.",
+ "103.200": "200 NOOP ok.",
+ "104.OTHERS": "250 Directory successfully changed.",
+ "105.OTHERS": "257 \"/pub/software/chinese\"",
+ "106.OTHERS": "250 Directory successfully changed.",
+ "107.200": "200 Switching to Binary mode.",
+ "108.227": "227 Entering Passive Mode (202,38,97,230,221,197).",
+ "109.OTHERS": "213 5079386",
+ "110.OTHERS": "150 Opening BINARY mode data connection for zhcon-0.2.3-1.i386.rpm (5079386 bytes).",
+ "111.OTHERS": "226 Transfer complete.",
+ "112.OTHERS": "250 Directory successfully changed."
+ },
+ {
+ "topic": "control_dtp",
+ "session": "192.168.32.27:64299-202.38.97.230:21-6-0",
+ "0.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "1.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "uri": "ftp://202.38.97.230/sjtu.edu.cn.html",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ },
+ "2.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "3.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "4.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "LIST"
+ },
+ "5.dtp": {
+ "username": "anonymous",
+ "password": "IEUser@",
+ "uri": "ftp://202.38.97.230/pub/software/chinese/zhcon-0.2.3-1.i386.rpm",
+ "dir": "retr",
+ "mode": "pasv",
+ "cmd": "RETR"
+ }
+ }
+] \ No newline at end of file
diff --git a/test/decoders/ftp/json/empty.json b/test/decoders/ftp/json/empty.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/test/decoders/ftp/json/empty.json
@@ -0,0 +1 @@
+[]
diff --git a/test/decoders/ftp/pcap/01-ftp-port-upload-download.pcap b/test/decoders/ftp/pcap/01-ftp-port-upload-download.pcap
new file mode 100644
index 0000000..314fa6f
--- /dev/null
+++ b/test/decoders/ftp/pcap/01-ftp-port-upload-download.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/01-ftp-port-upload-download_C2S.pcap b/test/decoders/ftp/pcap/01-ftp-port-upload-download_C2S.pcap
new file mode 100644
index 0000000..d8f9b06
--- /dev/null
+++ b/test/decoders/ftp/pcap/01-ftp-port-upload-download_C2S.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/01-ftp-port-upload-download_S2C.pcap b/test/decoders/ftp/pcap/01-ftp-port-upload-download_S2C.pcap
new file mode 100644
index 0000000..c0e9cc7
--- /dev/null
+++ b/test/decoders/ftp/pcap/01-ftp-port-upload-download_S2C.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/02-ftp_v6_1.pcap b/test/decoders/ftp/pcap/02-ftp_v6_1.pcap
new file mode 100644
index 0000000..9b7a924
--- /dev/null
+++ b/test/decoders/ftp/pcap/02-ftp_v6_1.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/03-ipv6_eport_upload_download.pcap b/test/decoders/ftp/pcap/03-ipv6_eport_upload_download.pcap
new file mode 100644
index 0000000..167507a
--- /dev/null
+++ b/test/decoders/ftp/pcap/03-ipv6_eport_upload_download.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/04-ftp-banner-no-ftp-characters.pcap b/test/decoders/ftp/pcap/04-ftp-banner-no-ftp-characters.pcap
new file mode 100644
index 0000000..f646ff2
--- /dev/null
+++ b/test/decoders/ftp/pcap/04-ftp-banner-no-ftp-characters.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/05-only-ctrl-link.pcap b/test/decoders/ftp/pcap/05-only-ctrl-link.pcap
new file mode 100644
index 0000000..8a5da68
--- /dev/null
+++ b/test/decoders/ftp/pcap/05-only-ctrl-link.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/06-ftp_pasv-upload-download.pcap b/test/decoders/ftp/pcap/06-ftp_pasv-upload-download.pcap
new file mode 100644
index 0000000..1a391ae
--- /dev/null
+++ b/test/decoders/ftp/pcap/06-ftp_pasv-upload-download.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/06-ftp_pasv-upload-download_C2S.pcap b/test/decoders/ftp/pcap/06-ftp_pasv-upload-download_C2S.pcap
new file mode 100644
index 0000000..753f328
--- /dev/null
+++ b/test/decoders/ftp/pcap/06-ftp_pasv-upload-download_C2S.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/06-ftp_pasv-upload-download_S2C.pcap b/test/decoders/ftp/pcap/06-ftp_pasv-upload-download_S2C.pcap
new file mode 100644
index 0000000..5ccd508
--- /dev/null
+++ b/test/decoders/ftp/pcap/06-ftp_pasv-upload-download_S2C.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/07-ftp-start-with-syst-command.pcap b/test/decoders/ftp/pcap/07-ftp-start-with-syst-command.pcap
new file mode 100644
index 0000000..9f1dff6
--- /dev/null
+++ b/test/decoders/ftp/pcap/07-ftp-start-with-syst-command.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/08-ftp_EPSV_229_ipv4.pcap b/test/decoders/ftp/pcap/08-ftp_EPSV_229_ipv4.pcap
new file mode 100644
index 0000000..926613b
--- /dev/null
+++ b/test/decoders/ftp/pcap/08-ftp_EPSV_229_ipv4.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/10-ftp-sdedu.pcap b/test/decoders/ftp/pcap/10-ftp-sdedu.pcap
new file mode 100644
index 0000000..cf53ce9
--- /dev/null
+++ b/test/decoders/ftp/pcap/10-ftp-sdedu.pcap
Binary files differ
diff --git a/test/decoders/ftp/pcap/11-ftp-sjtu.pcap b/test/decoders/ftp/pcap/11-ftp-sjtu.pcap
new file mode 100644
index 0000000..648dd55
--- /dev/null
+++ b/test/decoders/ftp/pcap/11-ftp-sjtu.pcap
Binary files differ