/* log3gpp.c * Routines encapsulating/dumping 3gpp protocol logs. * The purpose of this format is to be able to log the 3GPP protocol stack on a mobile phone. * Copyright 2008, Vincent Helfre * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "config.h" #define WS_LOG_DOMAIN LOG_DOMAIN_WIRETAP #include #include #include #include "wtap-int.h" #include "file_wrappers.h" #include "log3gpp.h" #define MAX_FIRST_LINE_LENGTH 200 #define MAX_TIMESTAMP_LINE_LENGTH 100 #define MAX_LINE_LENGTH 65536 #define MAX_TIMESTAMP_LEN 32 #define MAX_SECONDS_CHARS 16 #define MAX_SUBSECOND_DECIMALS 4 #define MAX_PROTOCOL_NAME 64 #define MAX_PROTOCOL_PAR_STRING 64 /* 'u' or 'd' of a packet as read from file */ typedef enum packet_direction_t { uplink, downlink } packet_direction_t; typedef struct { time_t start_secs; guint32 start_usecs; } log3gpp_t; gint first_packet_offset; gchar firstline[MAX_FIRST_LINE_LENGTH]; gchar secondline[MAX_TIMESTAMP_LINE_LENGTH]; gint secondline_length = 0; /***********************************************************/ /* Transient data used for parsing */ /* 'Magic number' at start of 3gpp log files. */ static const gchar log3gpp_magic[] = "3GPP protocols transcript"; /* Protocol name of the packet that the packet was captured at */ static gchar protocol_name[MAX_PROTOCOL_NAME+1]; /* Optional string parameter giving info required for the protocol dissector */ static gchar protocol_parameters[MAX_PROTOCOL_PAR_STRING+1]; /************************************************************/ /* Functions called from wiretap core */ static gboolean log3gpp_read( wtap* wth, wtap_rec* rec, Buffer* buf, int* err, gchar** err_info, gint64* data_offset); static gboolean log3gpp_seek_read(struct wtap *wth, gint64 seek_off, wtap_rec *rec, Buffer *buf, int *err, gchar **err_info); /************************************************************/ /* Private helper functions */ static gboolean read_new_line(FILE_T fh, gint* length, gchar* buf, size_t bufsize, int* err, gchar** err_info); static gboolean parse_line(char* linebuff, gint line_length, gint *seconds, gint *useconds, long *data_offset, gint *data_chars, packet_direction_t *direction, gboolean *is_text_data); static int write_stub_header(guchar *frame_buffer, char *timestamp_string, packet_direction_t direction); static guchar hex_from_char(gchar c); /*not used static gchar char_from_hex(guchar hex);*/ static gboolean get_file_time_stamp(gchar* linebuff, time_t *secs, guint32 *usecs); static int log3gpp_file_type_subtype = -1; void register_log3gpp(void); /***************************************************************************/ /* Free log3gpp-specific capture info from file that was open for reading */ /***************************************************************************/ static void log3gpp_close(wtap* wth) { log3gpp_t* log3gpp = (log3gpp_t*)wth->priv; /* Also free this capture info */ g_free(log3gpp); wth->priv = NULL; } /********************************************/ /* Open file (for reading) */ /********************************************/ wtap_open_return_val log3gpp_open(wtap *wth, int *err, gchar **err_info _U_) { time_t timestamp; guint32 usecs; log3gpp_t *log3gpp; wtap_open_return_val retval; /* Buffer to hold a single text line read from the file */ static gchar linebuff[MAX_LINE_LENGTH]; gint firstline_length = 0; /* Clear errno before reading from the file */ errno = 0; /********************************************************************/ /* First line needs to contain at least as many characters as magic */ /*ws_warning("Open file"); */ if (!read_new_line(wth->fh, &firstline_length, linebuff, sizeof linebuff, err, err_info)) { if (*err != 0 && *err != WTAP_ERR_SHORT_READ) { return WTAP_OPEN_ERROR; } else { return WTAP_OPEN_NOT_MINE; } } if (((size_t)firstline_length < strlen(log3gpp_magic)) || firstline_length >= MAX_FIRST_LINE_LENGTH) { retval = WTAP_OPEN_NOT_MINE; return retval; } /* This file is not for us if it doesn't match our signature */ if (memcmp(log3gpp_magic, linebuff, strlen(log3gpp_magic)) != 0) { retval = WTAP_OPEN_NOT_MINE; return retval; } /***********************************************************/ /* Second line contains file timestamp */ if (!read_new_line(wth->fh, &secondline_length, linebuff, sizeof linebuff, err, err_info)) { if (*err != 0 && *err != WTAP_ERR_SHORT_READ) { return WTAP_OPEN_ERROR; } else { return WTAP_OPEN_NOT_MINE; } } first_packet_offset = firstline_length + secondline_length; if ((secondline_length >= MAX_TIMESTAMP_LINE_LENGTH) || (!get_file_time_stamp(linebuff, ×tamp, &usecs))) { /* Give up if file time line wasn't valid */ retval = WTAP_OPEN_NOT_MINE; return retval; } /* Allocate struct and fill in timestamp (netmon re used)*/ log3gpp = g_new(log3gpp_t, 1); log3gpp->start_secs = timestamp; log3gpp->start_usecs = usecs; wth->priv = (void *)log3gpp; /************************************************************/ /* File is for us. Fill in details so packets can be read */ /* Set our file type */ wth->file_type_subtype = log3gpp_file_type_subtype; /* Use our own encapsulation to send all packets to our stub dissector */ wth->file_encap = WTAP_ENCAP_LOG_3GPP; /* Callbacks for reading operations */ wth->subtype_read = log3gpp_read; wth->subtype_seek_read = log3gpp_seek_read; wth->subtype_close = log3gpp_close; /* Choose microseconds (have 4 decimal places...) */ wth->file_tsprec = WTAP_TSPREC_USEC; *err = errno; /* * Add an IDB; we don't know how many interfaces were * involved, so we just say one interface, about which * we only know the link-layer type, snapshot length, * and time stamp resolution. */ wtap_add_generated_idb(wth); retval = WTAP_OPEN_MINE; return retval; } /**************************************************/ /* Read packet function. */ /* Look for and read the next usable packet */ /* - return TRUE and details if found */ /**************************************************/ gboolean log3gpp_read(wtap* wth, wtap_rec* rec, Buffer* buf, int* err, gchar** err_info, gint64* data_offset) { gint64 offset = file_tell(wth->fh); static gchar linebuff[MAX_LINE_LENGTH + 1]; long dollar_offset; packet_direction_t direction; gboolean is_text_data; log3gpp_t *log3gpp = (log3gpp_t *)wth->priv; /* Search for a line containing a usable packet */ while (1) { int line_length, seconds, useconds, data_chars; gint64 this_offset = offset; /* Are looking for first packet after 2nd line */ if (file_tell(wth->fh) == 0) { this_offset += (gint64)first_packet_offset +1+1; } /* Clear errno before reading from the file */ errno = 0; /* Read a new line from file into linebuff */ if (!read_new_line(wth->fh, &line_length, linebuff, sizeof linebuff, err, err_info)) { if (*err != 0) { return FALSE; /* error */ } /* No more lines can be read, so quit. */ break; } /* Try to parse the line as a frame record */ if (parse_line(linebuff, line_length, &seconds, &useconds, &dollar_offset, &data_chars, &direction, &is_text_data)) { guchar *frame_buffer; int n; int stub_offset = 0; char timestamp_string[MAX_TIMESTAMP_LEN+1]; /*not used gint64 *pkey = NULL;*/ snprintf(timestamp_string, 32, "%d.%04d", seconds, useconds/100); /* All packets go to 3GPP protocol stub dissector */ rec->rec_header.packet_header.pkt_encap = WTAP_ENCAP_LOG_3GPP; rec->rec_type = REC_TYPE_PACKET; rec->block = wtap_block_create(WTAP_BLOCK_PACKET); rec->presence_flags = WTAP_HAS_TS; /* Set data_offset to the beginning of the line we're returning. This will be the seek_off parameter when this frame is re-read. */ *data_offset = this_offset; /* Fill in timestamp (capture base + packet offset) */ rec->ts.secs = log3gpp->start_secs + seconds; if ((log3gpp->start_usecs + useconds) >= 1000000) { rec->ts.secs++; } rec->ts.nsecs = ((log3gpp->start_usecs + useconds) % 1000000) *1000; if (!is_text_data) { /* Get buffer pointer ready */ ws_buffer_assure_space(buf, strlen(timestamp_string)+1 + /* timestamp */ strlen(protocol_name)+1 + /* Protocol name */ 1 + /* direction */ (size_t)(data_chars/2)); frame_buffer = ws_buffer_start_ptr(buf); /*********************/ /* Write stub header */ stub_offset = write_stub_header(frame_buffer, timestamp_string, direction); /* Binary data length is half bytestring length + stub header */ rec->rec_header.packet_header.len = data_chars/2 + stub_offset; rec->rec_header.packet_header.caplen = data_chars/2 + stub_offset; /********************************/ /* Copy packet data into buffer */ for (n=0; n <= data_chars; n+=2) { frame_buffer[stub_offset + n/2] = (hex_from_char(linebuff[dollar_offset+n]) << 4) | hex_from_char(linebuff[dollar_offset+n+1]); } *err = errno = 0; return TRUE; } else { /* Get buffer pointer ready */ ws_buffer_assure_space(buf, strlen(timestamp_string)+1 + /* timestamp */ strlen(protocol_name)+1 + /* Protocol name */ 1 + /* direction */ data_chars); frame_buffer = ws_buffer_start_ptr(buf); /*********************/ /* Write stub header */ stub_offset = write_stub_header(frame_buffer, timestamp_string, direction); /* Binary data length is bytestring length + stub header */ rec->rec_header.packet_header.len = data_chars + stub_offset; rec->rec_header.packet_header.caplen = data_chars + stub_offset; /* do not convert the ascii char */ memcpy(&frame_buffer[stub_offset],&linebuff[dollar_offset],data_chars); frame_buffer[stub_offset+data_chars-1]= '\0'; *err = errno = 0; return TRUE; } } } /* No packet details to return... */ *err = errno; return FALSE; } /**************************************************/ /* Read & seek function. */ /**************************************************/ static gboolean log3gpp_seek_read(wtap *wth, gint64 seek_off, wtap_rec *rec _U_ , Buffer *buf, int *err, gchar **err_info) { long dollar_offset; static gchar linebuff[MAX_LINE_LENGTH + 1]; packet_direction_t direction; int seconds, useconds, data_chars; gboolean is_text_data; log3gpp_t* log3gpp = (log3gpp_t*)wth->priv; int length = 0; guchar *frame_buffer; /* Reset errno */ *err = errno = 0; /* Seek to beginning of packet */ if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1) { return FALSE; } /* Re-read whole line (this really should succeed) */ if (!read_new_line(wth->random_fh, &length, linebuff, sizeof linebuff, err, err_info)) { return FALSE; } /* Try to parse this line again (should succeed as re-reading...) */ if (parse_line(linebuff, length, &seconds, &useconds, &dollar_offset, &data_chars, &direction, &is_text_data)) { int n; int stub_offset = 0; char timestamp_string[32]; snprintf(timestamp_string, 32, "%d.%04d", seconds, useconds/100); /* Make sure all packets go to log3gpp dissector */ rec->rec_header.packet_header.pkt_encap = WTAP_ENCAP_LOG_3GPP; rec->rec_type = REC_TYPE_PACKET; rec->block = wtap_block_create(WTAP_BLOCK_PACKET); rec->presence_flags = WTAP_HAS_TS; /* Fill in timestamp (capture base + packet offset) */ rec->ts.secs = log3gpp->start_secs + seconds; if ((log3gpp->start_usecs + useconds) >= 1000000) { rec->ts.secs++; } rec->ts.nsecs = ((log3gpp->start_usecs + useconds) % 1000000) * 1000; /*********************/ /* Write stub header */ ws_buffer_assure_space(buf, strlen(timestamp_string)+1 + /* timestamp */ strlen(protocol_name)+1 + /* Protocol name */ 1 + /* direction */ data_chars); frame_buffer = ws_buffer_start_ptr(buf); stub_offset = write_stub_header(frame_buffer, timestamp_string, direction); if (!is_text_data) { /********************************/ /* Copy packet data into buffer */ for (n=0; n <= data_chars; n+=2) { frame_buffer[stub_offset + n/2] = (hex_from_char(linebuff[dollar_offset+n]) << 4) | hex_from_char(linebuff[dollar_offset+n+1]); } *err = errno = 0; return TRUE; } else { /* do not convert the ascii char */ memcpy(&frame_buffer[stub_offset],&linebuff[dollar_offset],data_chars); frame_buffer[stub_offset+data_chars-1] = '\0'; *err = errno = 0; return TRUE; } } /* If get here, must have failed */ *err = errno; *err_info = ws_strdup_printf("prot 3gpp: seek_read failed to read/parse " "line at position %" PRId64, seek_off); return FALSE; } /****************************/ /* Private helper functions */ /****************************/ /**********************************************************************/ /* Read a new line from the file, starting at offset. */ /* - writes data to static var linebuff */ /* - on return 'offset' will point to the next position to read from */ /* - return TRUE if this read is successful */ /**********************************************************************/ static gboolean read_new_line(FILE_T fh, gint* length, gchar* linebuff, size_t linebuffsize, int* err, gchar** err_info) { /* Read in a line */ gint64 pos_before = file_tell(fh); if (file_gets(linebuff, (int)linebuffsize - 1, fh) == NULL) { /* No characters found, or error */ *err = file_error(fh, err_info); return FALSE; } /* Set length (avoiding strlen()) and offset.. */ *length = (gint)(file_tell(fh) - pos_before); /* ...but don't want to include newline in line length */ if (*length > 0 && linebuff[*length - 1] == '\n') { linebuff[*length - 1] = '\0'; *length = *length - 1; } /* Nor do we want '\r' (as will be written when log is created on windows) */ if (*length > 0 && linebuff[*length - 1] == '\r') { linebuff[*length - 1] = '\0'; *length = *length - 1; } return TRUE; } /**********************************************************************/ /* Parse a line from buffer, by identifying: */ /* - timestamp */ /* - data position and length */ /* Return TRUE if this packet looks valid and can be displayed */ /**********************************************************************/ gboolean parse_line(gchar* linebuff, gint line_length, gint *seconds, gint *useconds, long *data_offset, gint *data_chars, packet_direction_t *direction, gboolean *is_text_data) { int n = 0; int protocol_chars = 0; int prot_option_chars = 0; char seconds_buff[MAX_SECONDS_CHARS+1]; int seconds_chars; char subsecond_decimals_buff[MAX_SUBSECOND_DECIMALS]; int subsecond_decimals_chars; /*********************************************************************/ /* Find and read the timestamp */ /*********************************************************************/ /* Now scan to the next digit, which should be the start of the timestamp */ for (; !g_ascii_isdigit((guchar)linebuff[n]) && (n < line_length); n++); if (n >= line_length) { return FALSE; } /* Seconds */ for (seconds_chars = 0; (linebuff[n] != '.') && (seconds_chars <= MAX_SECONDS_CHARS) && (n < line_length); n++, seconds_chars++) { if (!g_ascii_isdigit((guchar)linebuff[n])) { /* Found a non-digit before decimal point. Fail */ return FALSE; } seconds_buff[seconds_chars] = linebuff[n]; } if (seconds_chars > MAX_SECONDS_CHARS || n >= line_length) { /* Didn't fit in buffer. Fail rather than use truncated */ return FALSE; } /* Convert found value into number */ seconds_buff[seconds_chars] = '\0'; /* Already know they are digits, so avoid expense of ws_strtoi32() */ int multiplier = 1; *seconds = 0; for (int d = seconds_chars - 1; d >= 0; d--) { *seconds += ((seconds_buff[d] - '0') * multiplier); multiplier *= 10; } /* The decimal point must follow the last of the seconds digits */ if (linebuff[n] != '.') { return FALSE; } /* Skip it */ n++; /* Subsecond decimal digits (expect 4-digit accuracy) */ for (subsecond_decimals_chars = 0; (linebuff[n] != ' ') && (subsecond_decimals_chars < MAX_SUBSECOND_DECIMALS) && (n < line_length); n++, subsecond_decimals_chars++) { if (!g_ascii_isdigit((guchar)linebuff[n])) { return FALSE; } subsecond_decimals_buff[subsecond_decimals_chars] = linebuff[n]; } if (subsecond_decimals_chars > MAX_SUBSECOND_DECIMALS || n >= line_length) { /* More numbers than expected - give up */ return FALSE; } /* Convert found value into microseconds */ while (subsecond_decimals_chars < MAX_SUBSECOND_DECIMALS) { subsecond_decimals_buff[subsecond_decimals_chars++] = '0'; } /* Already know they are digits, so avoid expense of ws_strtoi32() */ *useconds = ((subsecond_decimals_buff[0] - '0') * 100000) + ((subsecond_decimals_buff[1] - '0') * 10000) + ((subsecond_decimals_buff[2] - '0') * 1000) + ((subsecond_decimals_buff[3] - '0') * 100); /* Space character must follow end of timestamp */ if (linebuff[n] != ' ') { return FALSE; } n++; /*********************************************************************/ /* Find and read protocol name */ /*********************************************************************/ for (protocol_chars = 0; (linebuff[n] != ' ') && (protocol_chars < MAX_PROTOCOL_NAME) && (n < line_length); n++, protocol_chars++) { if (!g_ascii_isalnum((guchar)linebuff[n]) && linebuff[n] != '_' && linebuff[n] != '.' && linebuff[n] != '-') { return FALSE; } protocol_name[protocol_chars] = linebuff[n]; } if (protocol_chars == MAX_PROTOCOL_NAME || n >= line_length) { /* If doesn't fit, fail rather than truncate */ return FALSE; } protocol_name[protocol_chars] = '\0'; /* Space char must follow protocol name */ if (linebuff[n] != ' ') { return FALSE; } /* Skip it */ n++; /* Scan ahead to the next space */ for (; (!g_ascii_isalnum((guchar)linebuff[n])) && (n < line_length); n++); if (n >= line_length) { return FALSE; } if (strcmp(protocol_name,"TXT") == 0) { *direction = uplink; *data_offset = n; *data_chars = line_length - n; *is_text_data = TRUE; } else { /* Next character gives direction of message (must be 'u' or 'd') */ if (linebuff[n] == 'u') { *direction = uplink; } else if (linebuff[n] == 'd') { *direction = downlink; } else { return FALSE; } n++; /* Now skip ahead to find start of data (marked by '$') */ for (; (n <= line_length) && (linebuff[n] != '$') && (prot_option_chars <= MAX_PROTOCOL_PAR_STRING); n++,prot_option_chars++) { protocol_parameters[prot_option_chars] = linebuff[n]; } protocol_parameters[prot_option_chars] = '\0'; if (prot_option_chars == MAX_PROTOCOL_PAR_STRING || n >= line_length) { /* If doesn't fit, fail rather than truncate */ return FALSE; } /* Skip it */ n++; /* Set offset to data start within line */ *data_offset = n; /* Set number of chars that comprise the hex string protocol data */ *data_chars = line_length - n; *is_text_data = FALSE; } return TRUE; } /*****************************************************************/ /* Write the stub info to the data buffer while reading a packet */ /*****************************************************************/ int write_stub_header(guchar *frame_buffer, char *timestamp_string, packet_direction_t direction) { int stub_offset = 0; /* Timestamp within file */ (void) g_strlcpy((char*)&frame_buffer[stub_offset], timestamp_string, MAX_TIMESTAMP_LEN+1); stub_offset += (int)(strlen(timestamp_string) + 1); /* Protocol name */ (void) g_strlcpy((char*)&frame_buffer[stub_offset], protocol_name, MAX_PROTOCOL_NAME+1); stub_offset += (int)(strlen(protocol_name) + 1); /* Direction */ frame_buffer[stub_offset] = direction; stub_offset++; /* Option string (might be string of length 0) */ (void) g_strlcpy((char*)&frame_buffer[stub_offset], protocol_parameters,MAX_PROTOCOL_PAR_STRING+1); stub_offset += (int)(strlen(protocol_parameters) + 1); return stub_offset; } /********************************************************/ /* Return hex nibble equivalent of hex string character */ /********************************************************/ guchar hex_from_char(gchar c) { if ((c >= '0') && (c <= '9')) { return c - '0'; } if ((c >= 'a') && (c <= 'f')) { return 0x0a + (c - 'a'); } if ((c >= 'A') && (c <= 'F')) { return 0x0a + (c - 'A'); } /* Not a valid hex string character */ return 0xff; } /********************************************************/ /* Return character corresponding to hex nibble value */ /********************************************************/ /*gchar char_from_hex(guchar hex) { static char hex_lookup[16] = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; if (hex > 15) { return '?'; } return hex_lookup[hex]; }*/ /************************************************************************/ /* Parse year, month, day, hour, minute, seconds out of formatted line. */ /* Set secs and usecs as output */ /* Return FALSE if no valid time can be read */ /************************************************************************/ gboolean get_file_time_stamp(gchar* linebuff, time_t *secs, guint32 *usecs) { int n; struct tm tm; #define MAX_MONTH_LETTERS 9 char month[MAX_MONTH_LETTERS+1]; int day, year, hour, minute, second; int scan_found; /* If line longer than expected, file is probably not correctly formatted */ if (strlen(linebuff) > MAX_TIMESTAMP_LINE_LENGTH) { return FALSE; } /**************************************************************/ /* First is month. Read until get a space following the month */ for (n=0; (n < MAX_MONTH_LETTERS) && (linebuff[n] != ' '); n++) { month[n] = linebuff[n]; } month[n] = '\0'; if (strcmp(month, "January" ) == 0) tm.tm_mon = 0; else if (strcmp(month, "February" ) == 0) tm.tm_mon = 1; else if (strcmp(month, "March" ) == 0) tm.tm_mon = 2; else if (strcmp(month, "April" ) == 0) tm.tm_mon = 3; else if (strcmp(month, "May" ) == 0) tm.tm_mon = 4; else if (strcmp(month, "June" ) == 0) tm.tm_mon = 5; else if (strcmp(month, "July" ) == 0) tm.tm_mon = 6; else if (strcmp(month, "August" ) == 0) tm.tm_mon = 7; else if (strcmp(month, "September") == 0) tm.tm_mon = 8; else if (strcmp(month, "October" ) == 0) tm.tm_mon = 9; else if (strcmp(month, "November" ) == 0) tm.tm_mon = 10; else if (strcmp(month, "December" ) == 0) tm.tm_mon = 11; else { /* Give up if not found a properly-formatted date */ return FALSE; } /* Skip space char */ n++; /********************************************************/ /* Scan for remaining numerical fields */ scan_found = sscanf(linebuff+n, "%d, %d %d:%d:%d.%u", &day, &year, &hour, &minute, &second, usecs); if (scan_found != 6) { /* Give up if not all found */ return FALSE; } /******************************************************/ /* Fill in remaining fields and return it in a time_t */ tm.tm_year = year - 1900; tm.tm_mday = day; tm.tm_hour = hour; tm.tm_min = minute; tm.tm_sec = second; tm.tm_isdst = -1; /* daylight saving time info not known */ /* Get seconds from this time */ *secs = mktime(&tm); /* Multiply 4 digits given to get micro-seconds */ *usecs = *usecs * 100; return TRUE; } static const struct supported_block_type log3gpp_blocks_supported[] = { /* * We support packet blocks, with no comments or other options. */ { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED } }; static const struct file_type_subtype_info log3gpp_info = { "3GPP Log", "3gpp_log", "*.log", NULL, TRUE, BLOCKS_SUPPORTED(log3gpp_blocks_supported), NULL, NULL, NULL }; void register_log3gpp(void) { log3gpp_file_type_subtype = wtap_register_file_type_subtype(&log3gpp_info); /* * Register name for backwards compatibility with the * wtap_filetypes table in Lua. */ wtap_register_backwards_compatibility_lua_name("LOG_3GPP", log3gpp_file_type_subtype); } /* * Editor modelines - https://www.wireshark.org/tools/modelines.html * * Local variables: * c-basic-offset: 4 * tab-width: 8 * indent-tabs-mode: nil * End: * * vi: set shiftwidth=4 tabstop=8 expandtab: * :indentSize=4:tabSize=8:noTabs=true: */