From fdefdcc6b90095393a6786d85646ffb7dfc46192 Mon Sep 17 00:00:00 2001 From: Darwvin Date: Sat, 23 May 2026 16:42:43 +0330 Subject: [PATCH] feat: add configurable Call-ID generators Summary: Add a new -cid_type option for built-in Call-ID generators while keeping -cid_str as the default behavior. This adds UUID, compact UUID, random, and timestamp-based Call-ID modes, along with unit coverage for each mode. Reasoning: -cid_str is flexible and should remain the default, but it still assumes callers want to describe the Call-ID shape as a formatting pattern. In practice there are cases where a ready-made generator is more convenient or more consistent: interoperability testing against systems that expect UUID-like identifiers, quickly switching between predictable and opaque identifiers during troubleshooting, and avoiding repeated hand-built templates for common styles. This change keeps backward compatibility by leaving -cid_str and the legacy formatting path untouched unless -cid_type is explicitly selected. The new modes provide a small set of built-in generators that cover the common cases without removing the existing customization path. --- include/sipp.hpp | 7 + src/call.cpp | 331 +++++++++++++++++++++++++++++++++++++++++++---- src/sipp.cpp | 48 ++++++- 3 files changed, 356 insertions(+), 30 deletions(-) diff --git a/include/sipp.hpp b/include/sipp.hpp index 6f2c7450..cf4c76bf 100644 --- a/include/sipp.hpp +++ b/include/sipp.hpp @@ -165,6 +165,12 @@ cmd messages are received */ #define DEFAULT_BEHAVIOR_PINGREPLY 4 #define DEFAULT_BEHAVIOR_BADCSEQ 8 +#define CID_MODE_FORMAT 0 +#define CID_MODE_UUID 1 +#define CID_MODE_UUID_COMPACT 2 +#define CID_MODE_RANDOM 3 +#define CID_MODE_TIMESTAMP 4 + #define DEFAULT_BEHAVIOR_ALL (DEFAULT_BEHAVIOR_BYE | DEFAULT_BEHAVIOR_ABORTUNEXP | DEFAULT_BEHAVIOR_PINGREPLY | DEFAULT_BEHAVIOR_BADCSEQ) #define DEFAULT_MIN_RTP_PORT DEFAULT_MEDIA_PORT @@ -290,6 +296,7 @@ MAYBE_EXTERN int currentRepartitionToDisplay DEFVAL(1); MAYBE_EXTERN unsigned int base_cseq DEFVAL(0); MAYBE_EXTERN char * auth_uri DEFVAL(0); MAYBE_EXTERN const char * call_id_string DEFVAL("%u-%p@%s"); +MAYBE_EXTERN int call_id_mode DEFVAL(CID_MODE_FORMAT); typedef std::unordered_map ParamMap; MAYBE_EXTERN ParamMap generic; diff --git a/src/call.cpp b/src/call.cpp index da80d5c0..b7c18073 100644 --- a/src/call.cpp +++ b/src/call.cpp @@ -39,8 +39,11 @@ */ #include +#include +#include #include #include +#include #include #include #include @@ -116,6 +119,165 @@ static const int SM_UNUSED = -1; static unsigned int next_number = 1; +class CallIdBuilder { +public: + explicit CallIdBuilder(unsigned int call_number) + : call_number(call_number) + { + } + + void build(char *call_id) + { + switch (call_id_mode) { + case CID_MODE_UUID: + build_uuid(false); + break; + case CID_MODE_UUID_COMPACT: + build_uuid(true); + break; + case CID_MODE_RANDOM: + build_random(); + break; + case CID_MODE_TIMESTAMP: + build_timestamp(); + break; + case CID_MODE_FORMAT: + default: + build_format(); + break; + } + + copy_to(call_id); + } + +private: + void build_format() + { + const char *src = call_id_string; + + while (*src) { + if (*src != '%') { + output << *src++; + continue; + } + + ++src; + if (*src == '\0') { + output << '%'; + break; + } + + switch (*src++) { + case 'u': + output << call_number; + break; + case 'p': + output << pid; + break; + case 's': + output << local_ip; + break; + case 'r': + output << rand(); + break; + default: + output << '%'; + break; + } + } + } + + void build_uuid(bool compact) + { + unsigned char bytes[16]; + + fill_bytes(bytes, sizeof(bytes)); + bytes[6] = static_cast((bytes[6] & 0x0f) | 0x40); + bytes[8] = static_cast((bytes[8] & 0x3f) | 0x80); + + append_hex_bytes(bytes, 4); + if (!compact) { + output << '-'; + } + append_hex_bytes(bytes + 4, 2); + if (!compact) { + output << '-'; + } + append_hex_bytes(bytes + 6, 2); + if (!compact) { + output << '-'; + } + append_hex_bytes(bytes + 8, 2); + if (!compact) { + output << '-'; + } + append_hex_bytes(bytes + 10, 6); + output << '@' << local_ip; + } + + void build_random() + { + unsigned char bytes[16]; + + fill_bytes(bytes, sizeof(bytes)); + output << call_number << '-'; + append_hex_bytes(bytes, sizeof(bytes)); + output << '@' << local_ip; + } + + void build_timestamp() + { + output << timestamp_micros() << '-' << call_number << '-' << pid << '@' << local_ip; + } + + void fill_bytes(unsigned char *bytes, size_t size) const + { + uint64_t seed = timestamp_micros(); + + seed ^= static_cast(call_number) << 32; + seed ^= static_cast(pid) << 8; + + for (size_t i = 0; i < size; ++i) { + seed ^= static_cast(rand()) << ((i % 4) * 8); + seed = seed * 2862933555777941757ULL + 3037000493ULL + i; + bytes[i] = static_cast((seed >> ((i % 8) * 8)) & 0xff); + } + } + + void append_hex_bytes(const unsigned char *bytes, size_t size) + { + for (size_t i = 0; i < size; ++i) { + output << std::hex << std::nouppercase << std::setw(2) << std::setfill('0') + << static_cast(bytes[i]); + } + output << std::dec << std::setfill(' '); + } + + static uint64_t timestamp_micros() + { + using clock = std::chrono::system_clock; + return static_cast( + std::chrono::duration_cast( + clock::now().time_since_epoch()).count()); + } + + void copy_to(char *call_id) const + { + std::string value = output.str(); + size_t length = std::min(value.size(), static_cast(MAX_HEADER_LEN - 1)); + memcpy(call_id, value.data(), length); + call_id[length] = '\0'; + } + + unsigned int call_number; + std::ostringstream output; +}; + +static void build_call_id(char *call_id, unsigned int call_number) +{ + CallIdBuilder(call_number).build(call_id); +} + static unsigned int get_tdm_map_number() { unsigned int nb = 0; @@ -791,38 +953,11 @@ call *call::add_call(int userId, bool ipv6, struct sockaddr_storage *dest) { static char call_id[MAX_HEADER_LEN]; - const char * src = call_id_string; - int count = 0; - if(!next_number) { next_number ++; } - while (*src && count < MAX_HEADER_LEN-1) { - if (*src == '%') { - ++src; - switch(*src++) { - case 'u': - count += snprintf(&call_id[count], MAX_HEADER_LEN-count-1, "%u", next_number); - break; - case 'p': - count += snprintf(&call_id[count], MAX_HEADER_LEN-count-1, "%u", pid); - break; - case 's': - count += snprintf(&call_id[count], MAX_HEADER_LEN-count-1, "%s", local_ip); - break; - case 'r': - count += snprintf(&call_id[count], MAX_HEADER_LEN-count-1, "%u", rand()); - break; - default: // treat all unknown sequences as %% - call_id[count++] = '%'; - break; - } - } else { - call_id[count++] = *src++; - } - } - call_id[count] = 0; + build_call_id(call_id, next_number); return new call(main_scenario, nullptr, dest, call_id, userId, ipv6, false /* Not Auto. */, false); } @@ -6837,6 +6972,146 @@ const std::string test_sdp_v6 = "v=0\r\n" "m=audio 12345 RTP/AVP 0\r\n" "a=rtpmap:0 PCMU/8000\r\n"; +static std::string call_id_body(const char *call_id) +{ + const char *separator = strchr(call_id, '@'); + if (!separator) { + return call_id; + } + return std::string(call_id, separator - call_id); +} + +static bool is_lower_hex_string(const std::string &value) +{ + return std::all_of(value.begin(), value.end(), [](unsigned char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); + }); +} + +class call_id_test_state_guard { +public: + call_id_test_state_guard() + : saved_call_id_string(call_id_string), + saved_call_id_mode(call_id_mode), + saved_pid(pid) + { + strcpy(saved_local_ip, local_ip); + } + + ~call_id_test_state_guard() + { + call_id_string = saved_call_id_string; + call_id_mode = saved_call_id_mode; + pid = saved_pid; + strcpy(local_ip, saved_local_ip); + } + +private: + const char *saved_call_id_string; + int saved_call_id_mode; + unsigned int saved_pid; + char saved_local_ip[sizeof(local_ip)]; +}; + +TEST(call_id, default_mode_uses_cid_str_template) { + char call_id[MAX_HEADER_LEN]; + call_id_test_state_guard guard; + + call_id_mode = CID_MODE_FORMAT; + call_id_string = "%u-%p@%s"; + pid = 4321; + strcpy(local_ip, "192.0.2.10"); + + build_call_id(call_id, 77); + EXPECT_STREQ("77-4321@192.0.2.10", call_id); +} + +TEST(call_id, default_mode_treats_trailing_percent_as_literal) { + char call_id[MAX_HEADER_LEN]; + call_id_test_state_guard guard; + + call_id_mode = CID_MODE_FORMAT; + call_id_string = "call-%"; + + build_call_id(call_id, 77); + EXPECT_STREQ("call-%", call_id); +} + +TEST(call_id, uuid_mode_generates_rfc4122_value) { + char call_id[MAX_HEADER_LEN]; + call_id_test_state_guard guard; + + call_id_mode = CID_MODE_UUID; + pid = 9001; + strcpy(local_ip, "198.51.100.25"); + + build_call_id(call_id, 15); + + std::string body = call_id_body(call_id); + ASSERT_EQ(body.size(), 36U); + EXPECT_EQ(body[8], '-'); + EXPECT_EQ(body[13], '-'); + EXPECT_EQ(body[18], '-'); + EXPECT_EQ(body[23], '-'); + EXPECT_EQ(body[14], '4'); + EXPECT_TRUE(body[19] == '8' || body[19] == '9' || body[19] == 'a' || body[19] == 'b'); + EXPECT_TRUE(is_lower_hex_string(body.substr(0, 8))); + EXPECT_TRUE(is_lower_hex_string(body.substr(9, 4))); + EXPECT_TRUE(is_lower_hex_string(body.substr(14, 4))); + EXPECT_TRUE(is_lower_hex_string(body.substr(19, 4))); + EXPECT_TRUE(is_lower_hex_string(body.substr(24, 12))); + EXPECT_NE(std::string(call_id).find("@198.51.100.25"), std::string::npos); +} + +TEST(call_id, uuid_compact_mode_generates_hex_value) { + char call_id[MAX_HEADER_LEN]; + call_id_test_state_guard guard; + + call_id_mode = CID_MODE_UUID_COMPACT; + pid = 1337; + strcpy(local_ip, "203.0.113.8"); + + build_call_id(call_id, 22); + + std::string body = call_id_body(call_id); + ASSERT_EQ(body.size(), 32U); + EXPECT_TRUE(is_lower_hex_string(body)); + EXPECT_EQ(body[12], '4'); + EXPECT_TRUE(body[16] == '8' || body[16] == '9' || body[16] == 'a' || body[16] == 'b'); + EXPECT_NE(std::string(call_id).find("@203.0.113.8"), std::string::npos); +} + +TEST(call_id, random_mode_generates_unique_token_with_call_number) { + char call_id[MAX_HEADER_LEN]; + call_id_test_state_guard guard; + + call_id_mode = CID_MODE_RANDOM; + pid = 5150; + strcpy(local_ip, "203.0.113.44"); + + build_call_id(call_id, 31); + + std::string body = call_id_body(call_id); + ASSERT_EQ(body.rfind("31-", 0), 0U); + ASSERT_EQ(body.size(), 35U); + EXPECT_TRUE(is_lower_hex_string(body.substr(3))); + EXPECT_NE(std::string(call_id).find("@203.0.113.44"), std::string::npos); +} + +TEST(call_id, timestamp_mode_embeds_call_number_and_pid) { + char call_id[MAX_HEADER_LEN]; + call_id_test_state_guard guard; + + call_id_mode = CID_MODE_TIMESTAMP; + pid = 2718; + strcpy(local_ip, "192.0.2.44"); + + build_call_id(call_id, 52); + + std::string full_call_id(call_id); + EXPECT_NE(full_call_id.find("-52-2718@192.0.2.44"), std::string::npos); +} + TEST(sdp, parse_valid_sdp_msg) { ASSERT_EQ(find_in_sdp("c=IN IP4 ", test_sdp_v4), "127.0.0.1"); ASSERT_EQ(find_in_sdp("c=IN IP6 ", test_sdp_v6), "::1"); diff --git a/src/sipp.cpp b/src/sipp.cpp index b4ed127a..d8795224 100644 --- a/src/sipp.cpp +++ b/src/sipp.cpp @@ -40,11 +40,13 @@ #include #include #include +#include #include +#include #include -#include -#include #include +#include +#include #ifdef __APPLE__ /* Provide OSX version of extern char **environ; */ @@ -124,6 +126,43 @@ struct sipp_option { #define SIPP_OPTION_RX_INPUT_FILE 41 #define SIPP_HELP_TEXT_HEADER 255 +static char *call_id_mode_string = nullptr; + +static std::string lowercase_copy(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return value; +} + +static bool parse_call_id_mode(const char *value, int *mode) +{ + std::string lowered = lowercase_copy(value); + + if (lowered == "default" || lowered == "format" || lowered == "legacy") { + *mode = CID_MODE_FORMAT; + return true; + } + if (lowered == "uuid") { + *mode = CID_MODE_UUID; + return true; + } + if (lowered == "uuid-compact" || lowered == "uuidcompact" || lowered == "uuid32") { + *mode = CID_MODE_UUID_COMPACT; + return true; + } + if (lowered == "random" || lowered == "random-hex") { + *mode = CID_MODE_RANDOM; + return true; + } + if (lowered == "timestamp" || lowered == "time") { + *mode = CID_MODE_TIMESTAMP; + return true; + } + + return false; +} + /* Put each option, its help text, and type in this table. */ struct sipp_option options_table[] = { {"h", nullptr, SIPP_OPTION_HELP, nullptr, 0}, @@ -234,6 +273,7 @@ struct sipp_option options_table[] = { {"aa", "Enable automatic 200 OK answer for INFO, NOTIFY, OPTIONS and UPDATE.", SIPP_OPTION_SETFLAG, &auto_answer, 1}, {"base_cseq", "Start value of [cseq] for each call.", SIPP_OPTION_CSEQ, nullptr, 1}, {"cid_str", "Call ID string (default %u-%p@%s). %u=call_number, %s=ip_address, %p=process_number, %r=random_integer, %%=% (in any order).", SIPP_OPTION_STRING, &call_id_string, 1}, + {"cid_type", "Call ID generation mode. Values: default (aliases: format, legacy), uuid, uuid-compact (aliases: uuidcompact, uuid32), random (alias: random-hex), timestamp (alias: time). Modes other than default ignore -cid_str.", SIPP_OPTION_STRING, &call_id_mode_string, 1}, {"d", "Controls the length of calls. More precisely, this controls the duration of 'pause' instructions in the scenario, if they do not have a 'milliseconds' section. Default value is 0 and default unit is milliseconds.", SIPP_OPTION_TIME_MS, &duration, 1}, {"deadcall_wait", "How long the Call-ID and final status of calls should be kept to improve message and error logs (default unit is ms).", SIPP_OPTION_TIME_MS, &deadcall_wait, 1}, {"auth_uri", "Force the value of the URI for authentication.\n" @@ -1940,6 +1980,10 @@ int main(int argc, char *argv[]) } } + if (call_id_mode_string && !parse_call_id_mode(call_id_mode_string, &call_id_mode)) { + ERROR("Unknown Call-ID mode '%s'. Use default, format, legacy, uuid, uuid-compact, uuidcompact, uuid32, random, random-hex, timestamp, or time.", call_id_mode_string); + } + /* generate random ssrc */ if (random_base_ssrc) { global_ssrc_id = rand();