Skip to content

Commit

Permalink
SIP: extract some basic metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanNardi committed Nov 12, 2024
1 parent 9425655 commit c9279d1
Show file tree
Hide file tree
Showing 20 changed files with 268 additions and 21 deletions.
4 changes: 4 additions & 0 deletions doc/configuration_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ TODO
| "imap" | "tls_dissection" | enable | NULL | NULL | Enable/disable dissection of TLS packets in cleartext IMAP flows (because of opportunistic TLS, via STARTTLS msg) |
| "pop" | "tls_dissection" | enable | NULL | NULL | Enable/disable dissection of TLS packets in cleartext POP flows (because of opportunistic TLS, via STARTTLS msg) |
| "ftp" | "tls_dissection" | enable | NULL | NULL | Enable/disable dissection of TLS packets in cleartext FTP flows (because of opportunistic TLS, via AUTH TLS msg) |
| "sip" | "metadata.attribute.from" | enable | NULL | NULL | Enable/disable extraction of "From" header from SIP flows |
| "sip" | "metadata.attribute.from_imsi" | enable | NULL | NULL | In a SIP flow, if the "From" header contains a valid IMSI, this option enable/disable the extraction of the IMSI itself |
| "sip" | "metadata.attribute.to" | enable | NULL | NULL | Enable/disable extraction of "To" header from SIP flows |
| "sip" | "metadata.attribute.to_imsi" | enable | NULL | NULL | In a SIP flow, if the "To" header contains a valid IMSI, this option enable/disable the extraction of the IMSI itself |
| "stun" | "max_packets_extra_dissection" | 4 | 0 | 255 | After a flow has been classified has STUN, nDPI might analyse more packets to look for a sub-classification or for metadata. This parameter set the upper limit on the number of these packets |
| "stun" | "tls_dissection" | enable | NULL | NULL | Enable/disable dissection of TLS packets multiplexed into STUN flows |
| "stun" | "metadata.attribute.mapped_address" | enable | NULL | NULL | Enable/disable extraction of (xor)-mapped-address attribute for STUN flows. If it is disabled, STUN classification might be significant faster |
Expand Down
19 changes: 19 additions & 0 deletions example/ndpiReader.c
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,25 @@ static void printFlow(u_int32_t id, struct ndpi_flow_info *flow, u_int16_t threa
}
break;

case INFO_SIP:
if (flow->sip.from[0] != '\0')
{
fprintf(out, "[SIP From: %s]", flow->sip.from);
}
if (flow->sip.from_imsi[0] != '\0')
{
fprintf(out, "[SIP From IMSI: %s]", flow->sip.from_imsi);
}
if (flow->sip.to[0] != '\0')
{
fprintf(out, "[SIP To: %s]", flow->sip.to);
}
if (flow->sip.to_imsi[0] != '\0')
{
fprintf(out, "[SIP To IMSI: %s]", flow->sip.to_imsi);
}
break;

case INFO_NATPMP:
if (flow->natpmp.internal_port != 0 && flow->natpmp.ip[0] != '\0')
{
Expand Down
12 changes: 12 additions & 0 deletions example/reader_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,18 @@ void process_ndpi_collected_info(struct ndpi_workflow * workflow, struct ndpi_fl
ndpi_snprintf(flow->info, sizeof(flow->info), "Username: %s",
flow->ndpi_flow->protos.collectd.client_username);
}
/* SIP */
else if(is_ndpi_proto(flow, NDPI_PROTOCOL_SIP)) {
flow->info_type = INFO_SIP;
if(flow->ndpi_flow->protos.sip.from)
ndpi_snprintf(flow->sip.from, sizeof(flow->sip.from), "%s", flow->ndpi_flow->protos.sip.from);
if(flow->ndpi_flow->protos.sip.from_imsi[0] != '\0')
ndpi_snprintf(flow->sip.from_imsi, sizeof(flow->sip.from_imsi), "%s", flow->ndpi_flow->protos.sip.from_imsi);
if(flow->ndpi_flow->protos.sip.to)
ndpi_snprintf(flow->sip.to, sizeof(flow->sip.to), "%s", flow->ndpi_flow->protos.sip.to);
if(flow->ndpi_flow->protos.sip.to_imsi[0] != '\0')
ndpi_snprintf(flow->sip.to_imsi, sizeof(flow->sip.to_imsi), "%s", flow->ndpi_flow->protos.sip.to_imsi);
}
/* TELNET */
else if(is_ndpi_proto(flow, NDPI_PROTOCOL_TELNET)) {
if(flow->ndpi_flow->protos.telnet.username[0] != '\0')
Expand Down
8 changes: 8 additions & 0 deletions example/reader_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ enum info_type {
INFO_TIVOCONNECT,
INFO_FTP_IMAP_POP_SMTP,
INFO_NATPMP,
INFO_SIP,
};

typedef struct {
Expand Down Expand Up @@ -262,6 +263,13 @@ typedef struct ndpi_flow_info {
uint16_t external_port;
char ip[16];
} natpmp;

struct {
char from[256];
char from_imsi[16];
char to[256];
char to_imsi[16];
} sip;
};

ndpi_serializer ndpi_flow_serializer;
Expand Down
20 changes: 20 additions & 0 deletions fuzz/fuzz_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,26 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
snprintf(cfg_value, sizeof(cfg_value), "%d", value);
ndpi_set_config(ndpi_info_mod, "ftp", "tls_dissection", cfg_value);
}
if(fuzzed_data.ConsumeBool()) {
value = fuzzed_data.ConsumeIntegralInRange(0, 1 + 1);
snprintf(cfg_value, sizeof(cfg_value), "%d", value);
ndpi_set_config(ndpi_info_mod, "sip", "metadata.attribute.from", cfg_value);
}
if(fuzzed_data.ConsumeBool()) {
value = fuzzed_data.ConsumeIntegralInRange(0, 1 + 1);
snprintf(cfg_value, sizeof(cfg_value), "%d", value);
ndpi_set_config(ndpi_info_mod, "sip", "metadata.attribute.from_imsi", cfg_value);
}
if(fuzzed_data.ConsumeBool()) {
value = fuzzed_data.ConsumeIntegralInRange(0, 1 + 1);
snprintf(cfg_value, sizeof(cfg_value), "%d", value);
ndpi_set_config(ndpi_info_mod, "sip", "metadata.attribute.to", cfg_value);
}
if(fuzzed_data.ConsumeBool()) {
value = fuzzed_data.ConsumeIntegralInRange(0, 1 + 1);
snprintf(cfg_value, sizeof(cfg_value), "%d", value);
ndpi_set_config(ndpi_info_mod, "sip", "metadata.attribute.to_imsi", cfg_value);
}
if(fuzzed_data.ConsumeBool()) {
value = fuzzed_data.ConsumeIntegralInRange(0, 1 + 1);
snprintf(cfg_value, sizeof(cfg_value), "%d", value);
Expand Down
2 changes: 2 additions & 0 deletions src/include/ndpi_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ extern "C" {
void ndpi_flow_free(void *ptr);
u_int32_t ndpi_get_tot_allocated_memory(void);

char *ndpi_strip_leading_trailing_spaces(char *ptr, int *ptr_len) ;

/**
* Finds the first occurrence of the substring 'needle' in the string 'haystack'.
*
Expand Down
5 changes: 5 additions & 0 deletions src/include/ndpi_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ struct ndpi_detection_module_config_struct {

int ftp_opportunistic_tls_enabled;

int sip_attribute_from_enabled;
int sip_attribute_from_imsi_enabled;
int sip_attribute_to_enabled;
int sip_attribute_to_imsi_enabled;

int stun_opportunistic_tls_enabled;
int stun_max_packets_extra_dissection;
int stun_mapped_address_enabled;
Expand Down
7 changes: 7 additions & 0 deletions src/include/ndpi_typedefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,13 @@ struct ndpi_flow_struct {
u_int8_t url_count;
char url[4][48];
} slp;

struct {
char *from;
char from_imsi[16]; /* IMSI is 15 digit long, at most; + 1 for NULL terminator */
char *to;
char to_imsi[16];
} sip;
} protos;

/* **Packet** metadata for flows where monitoring is enabled. It is reset after each packet! */
Expand Down
12 changes: 12 additions & 0 deletions src/lib/ndpi_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -6806,6 +6806,13 @@ void ndpi_free_flow_data(struct ndpi_flow_struct* flow) {
ndpi_free(flow->protos.tls_quic.ja4_client_raw);
}

if(flow_is_proto(flow, NDPI_PROTOCOL_SIP)) {
if(flow->protos.sip.from)
ndpi_free(flow->protos.sip.from);
if(flow->protos.sip.to)
ndpi_free(flow->protos.sip.to);
}

if(flow->tls_quic.message[0].buffer)
ndpi_free(flow->tls_quic.message[0].buffer);
if(flow->tls_quic.message[1].buffer)
Expand Down Expand Up @@ -11498,6 +11505,11 @@ static const struct cfg_param {

{ "ftp", "tls_dissection", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(ftp_opportunistic_tls_enabled), NULL },

{ "sip", "metadata.attribute.from", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(sip_attribute_from_enabled), NULL },
{ "sip", "metadata.attribute.from_imsi", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(sip_attribute_from_imsi_enabled), NULL },
{ "sip", "metadata.attribute.to", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(sip_attribute_to_enabled), NULL },
{ "sip", "metadata.attribute.to_imsi", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(sip_attribute_to_imsi_enabled), NULL },

{ "stun", "tls_dissection", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(stun_opportunistic_tls_enabled), NULL },
{ "stun", "max_packets_extra_dissection", "6", "0", "255", CFG_PARAM_INT, __OFF(stun_max_packets_extra_dissection), NULL },
{ "stun", "metadata.attribute.mapped_address", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(stun_mapped_address_enabled), NULL },
Expand Down
22 changes: 22 additions & 0 deletions src/lib/ndpi_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -3902,3 +3902,25 @@ char* ndpi_strndup(const char *s, size_t size) {

return(ret);
}

/* ************************************************************** */

char *ndpi_strip_leading_trailing_spaces(char *ptr, int *ptr_len) {

/* Stripping leading spaces */
while(*ptr_len > 0 && ptr[0] == ' ') {
(*ptr_len)--;
ptr++;
}
if(*ptr_len == 0)
return NULL;

/* Stripping trailing spaces */
while(*ptr_len > 0 && ptr[*ptr_len - 1] == ' ') {
(*ptr_len)--;
}
if(*ptr_len == 0)
return NULL;

return ptr;
}
100 changes: 99 additions & 1 deletion src/lib/protocols/sip.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@
#include "ndpi_api.h"
#include "ndpi_private.h"

static void search_metadata(struct ndpi_detection_module_struct *ndpi_struct, struct ndpi_flow_struct *flow);

static void ndpi_int_sip_add_connection(struct ndpi_detection_module_struct *ndpi_struct,
struct ndpi_flow_struct *flow) {
ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_SIP, NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI);

search_metadata(ndpi_struct, flow);
}

/* ********************************************************** */
Expand Down Expand Up @@ -128,9 +132,103 @@ static int search_cmd(struct ndpi_detection_module_struct *ndpi_struct)
return 0;
}

/* ********************************************************** */

static char *get_imsi(const char *str, int *imsi_len)
{
char *s, *e, *c;

/* Format: <sip:[email protected]>;tag=YpUNxYCzz0dMHM */

s = ndpi_strnstr(str, "<sip:", strlen(str));
if(!s)
return NULL;
e = ndpi_strnstr(s, "@", strlen(s));
if(!e)
return NULL;
*imsi_len = e - s - 5;
/* IMSI is 14 or 15 digit length */
if(*imsi_len != 14 && *imsi_len != 15)
return NULL;
for(c = s + 5; c != e; c++)
if(!isdigit(*c))
return NULL;
return s + 5;
}

/* ********************************************************** */

static int metadata_enabled(struct ndpi_detection_module_struct *ndpi_struct)
{
/* At least one */
return ndpi_struct->cfg.sip_attribute_from_enabled ||
ndpi_struct->cfg.sip_attribute_from_imsi_enabled ||
ndpi_struct->cfg.sip_attribute_to_enabled ||
ndpi_struct->cfg.sip_attribute_to_imsi_enabled;
}

/* ********************************************************** */

static void search_metadata(struct ndpi_detection_module_struct *ndpi_struct, struct ndpi_flow_struct *flow)
{
struct ndpi_packet_struct *packet = &ndpi_struct->packet;
u_int16_t a;
int str_len, imsi_len;
char *str, *imsi;

if(!metadata_enabled(ndpi_struct))
return;

NDPI_PARSE_PACKET_LINE_INFO(ndpi_struct, flow, packet);

for(a = 0; a < packet->parsed_lines; a++) {
/* From */
if(ndpi_struct->cfg.sip_attribute_from_enabled &&
flow->protos.sip.from == NULL &&
packet->line[a].len >= 5 &&
memcmp(packet->line[a].ptr, "From:", 5) == 0) {
str_len = packet->line[a].len - 5;
str = ndpi_strip_leading_trailing_spaces((char *)packet->line[a].ptr + 5, &str_len);
if(str) {
NDPI_LOG_DBG2(ndpi_struct, "Found From: %.*s\n", str_len, str);
flow->protos.sip.from = ndpi_strndup(str, str_len);
if(ndpi_struct->cfg.sip_attribute_from_imsi_enabled &&
flow->protos.sip.from) {
imsi = get_imsi(flow->protos.sip.from, &imsi_len);
if(imsi) {
NDPI_LOG_DBG2(ndpi_struct, "Found From IMSI: %.*s\n", imsi_len, imsi);
memcpy(flow->protos.sip.from_imsi, imsi, imsi_len);
}
}
}
}

/* To */
if(ndpi_struct->cfg.sip_attribute_to_enabled &&
flow->protos.sip.to == NULL &&
packet->line[a].len >= 3 &&
memcmp(packet->line[a].ptr, "To:", 3) == 0) {
str_len = packet->line[a].len - 3;
str = ndpi_strip_leading_trailing_spaces((char *)packet->line[a].ptr + 3, &str_len);
if(str) {
NDPI_LOG_DBG2(ndpi_struct, "Found To: %.*s\n", str_len, str);
flow->protos.sip.to = ndpi_strndup(str, str_len);
if(ndpi_struct->cfg.sip_attribute_to_imsi_enabled &&
flow->protos.sip.to) {
imsi = get_imsi(flow->protos.sip.to, &imsi_len);
if(imsi) {
NDPI_LOG_DBG2(ndpi_struct, "Found To IMSI: %.*s\n", imsi_len, imsi);
memcpy(flow->protos.sip.to_imsi, imsi, imsi_len);
}
}
}
}
}
}

/* ********************************************************** */

void ndpi_search_sip(struct ndpi_detection_module_struct *ndpi_struct, struct ndpi_flow_struct *flow) {
static void ndpi_search_sip(struct ndpi_detection_module_struct *ndpi_struct, struct ndpi_flow_struct *flow) {
struct ndpi_packet_struct *packet = &ndpi_struct->packet;
const u_int8_t *packet_payload = packet->payload;
u_int32_t payload_len = packet->payload_packet_len;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Megaco 130 23570 1
Acceptable 1552 259123 5

1 UDP 10.35.60.100:15580 <-> 10.23.1.52:16756 [proto: 87/RTP][IP: 0/Unknown][Stream Content: Audio][ClearText][Confidence: DPI][FPC: 0/Unknown, Confidence: Unknown][DPI packets: 3][cat: Media/1][159 pkts/33872 bytes <-> 1171 pkts/148830 bytes][Goodput ratio: 80/66][37.44 sec][bytes ratio: -0.629 (Download)][IAT c2s/s2c min/avg/max/stddev: 0/0 20/30 81/286 7/49][Pkt Len c2s/s2c min/avg/max/stddev: 60/60 213/127 214/214 12/32][PLAIN TEXT (UUUUUU)][Plen Bins: 0,0,50,0,0,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
2 UDP 10.35.40.25:5060 <-> 10.35.40.200:5060 [proto: 100/SIP][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 100/SIP, Confidence: DPI][DPI packets: 1][cat: VoIP/10][22 pkts/13254 bytes <-> 24 pkts/13218 bytes][Goodput ratio: 93/92][83.79 sec][bytes ratio: 0.001 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 0/0 3385/1643 27628/17187 8177/4202][Pkt Len c2s/s2c min/avg/max/stddev: 425/304 602/551 923/894 205/186][PLAIN TEXT (INVITE sip)][Plen Bins: 0,0,0,0,0,0,0,0,4,0,8,4,22,18,4,0,8,0,0,0,0,0,0,4,8,4,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
2 UDP 10.35.40.25:5060 <-> 10.35.40.200:5060 [proto: 100/SIP][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 100/SIP, Confidence: DPI][DPI packets: 1][cat: VoIP/10][22 pkts/13254 bytes <-> 24 pkts/13218 bytes][Goodput ratio: 93/92][83.79 sec][SIP From: <sip:unavailable@hostportion>;tag=00e9d478][SIP To: <sip:[email protected];user=phone>][bytes ratio: 0.001 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 0/0 3385/1643 27628/17187 8177/4202][Pkt Len c2s/s2c min/avg/max/stddev: 425/304 602/551 923/894 205/186][PLAIN TEXT (INVITE sip)][Plen Bins: 0,0,0,0,0,0,0,0,4,0,8,4,22,18,4,0,8,0,0,0,0,0,0,4,8,4,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
3 UDP 10.35.40.22:2944 <-> 10.23.1.42:2944 [proto: 181/Megaco][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 181/Megaco, Confidence: DPI][DPI packets: 1][cat: VoIP/10][65 pkts/7788 bytes <-> 65 pkts/15782 bytes][Goodput ratio: 65/83][109.25 sec][bytes ratio: -0.339 (Download)][IAT c2s/s2c min/avg/max/stddev: 0/0 1409/1356 4370/4370 1953/1909][Pkt Len c2s/s2c min/avg/max/stddev: 77/101 120/243 583/561 107/94][PLAIN TEXT (555282713)][Plen Bins: 0,48,0,23,0,1,1,21,0,0,1,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
4 UDP 10.35.60.72:5060 <-> 10.35.60.100:5060 [proto: 100/SIP][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 100/SIP, Confidence: DPI][DPI packets: 1][cat: VoIP/10][11 pkts/6627 bytes <-> 12 pkts/6609 bytes][Goodput ratio: 93/92][83.79 sec][bytes ratio: 0.001 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 1/19 7451/3699 27579/17188 10544/5458][Pkt Len c2s/s2c min/avg/max/stddev: 425/304 602/551 923/894 205/186][PLAIN TEXT (INVITE sip)][Plen Bins: 0,0,0,0,0,0,0,0,4,0,8,4,22,18,4,0,8,0,0,0,0,0,0,4,8,4,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
5 UDP 138.132.169.101:5060 <-> 192.168.100.219:5060 [proto: 100/SIP][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 100/SIP, Confidence: DPI][DPI packets: 1][cat: VoIP/10][11 pkts/6498 bytes <-> 12 pkts/6645 bytes][Goodput ratio: 93/92][83.79 sec][bytes ratio: -0.011 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 21/16 7450/3691 27580/17187 10543/5469][Pkt Len c2s/s2c min/avg/max/stddev: 380/339 591/554 926/875 214/174][PLAIN TEXT (mINVITE sip)][Plen Bins: 0,0,0,0,0,0,0,0,0,4,13,0,27,13,4,0,8,0,0,0,0,0,0,4,8,4,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
4 UDP 10.35.60.72:5060 <-> 10.35.60.100:5060 [proto: 100/SIP][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 100/SIP, Confidence: DPI][DPI packets: 1][cat: VoIP/10][11 pkts/6627 bytes <-> 12 pkts/6609 bytes][Goodput ratio: 93/92][83.79 sec][SIP From: <sip:unavailable@hostportion>;tag=00e9d478][SIP To: <sip:[email protected];user=phone>][bytes ratio: 0.001 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 1/19 7451/3699 27579/17188 10544/5458][Pkt Len c2s/s2c min/avg/max/stddev: 425/304 602/551 923/894 205/186][PLAIN TEXT (INVITE sip)][Plen Bins: 0,0,0,0,0,0,0,0,4,0,8,4,22,18,4,0,8,0,0,0,0,0,0,4,8,4,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
5 UDP 138.132.169.101:5060 <-> 192.168.100.219:5060 [proto: 100/SIP][IP: 0/Unknown][ClearText][Confidence: DPI][FPC: 100/SIP, Confidence: DPI][DPI packets: 1][cat: VoIP/10][11 pkts/6498 bytes <-> 12 pkts/6645 bytes][Goodput ratio: 93/92][83.79 sec][SIP From: <sip:unavailable@hostportion>;tag=SD4909701-00e9d478][SIP To: <sip:[email protected];user=phone>][bytes ratio: -0.011 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 21/16 7450/3691 27580/17187 10543/5469][Pkt Len c2s/s2c min/avg/max/stddev: 380/339 591/554 926/875 214/174][PLAIN TEXT (mINVITE sip)][Plen Bins: 0,0,0,0,0,0,0,0,0,4,13,0,27,13,4,0,8,0,0,0,0,0,0,4,8,4,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
Loading

0 comments on commit c9279d1

Please sign in to comment.