Commit cc87e4b0 authored by Ghislain MARY's avatar Ghislain MARY

Handle SRV + A or AAAA query.

parent 55125622
......@@ -168,7 +168,7 @@ static int resolver_process_data(belle_sip_resolver_context_t *ctx, unsigned int
} else if ((ctx->type == DNS_T_SRV) && (rr.class == DNS_C_IN) && (rr.type == DNS_T_SRV)) {
struct dns_srv *srv = &any.srv;
struct dns_srv *res = belle_sip_malloc(sizeof(struct dns_srv));
memcpy(res, srv, sizeof(res));
memcpy(res, srv, sizeof(struct dns_srv));
snprintf(host, sizeof(host), "[target:%s port:%d prio:%d weight:%d]", srv->target, srv->port, srv->priority, srv->weight);
ctx->srv_list = belle_sip_list_append(ctx->srv_list, res);
belle_sip_message("SRV %s resolved to %s", ctx->name, host);
......@@ -176,13 +176,17 @@ static int resolver_process_data(belle_sip_resolver_context_t *ctx, unsigned int
}
}
free(ans);
ctx->done=TRUE;
if ((ctx->type == DNS_T_A) || (ctx->type == DNS_T_AAAA)) {
ctx->cb(ctx->cb_data, ctx->name, ctx->ai_list);
} else if (ctx->type == DNS_T_SRV) {
ctx->srv_cb(ctx->cb_data, ctx->name, ctx->srv_list);
}
ctx->done=TRUE;
return BELLE_SIP_STOP;
if (ctx->done == TRUE) {
return BELLE_SIP_STOP;
} else {
return BELLE_SIP_CONTINUE;
}
}
if (error != DNS_EAGAIN) {
belle_sip_error("%s dns_res_check error: %s (%d)", __FUNCTION__, dns_strerror(error), error);
......@@ -194,7 +198,7 @@ static int resolver_process_data(belle_sip_resolver_context_t *ctx, unsigned int
return BELLE_SIP_CONTINUE;
}
static int _resolver_start_query(belle_sip_resolver_context_t *ctx, belle_sip_source_func_t datafunc, int timeout) {
static int _resolver_send_query(belle_sip_resolver_context_t *ctx, belle_sip_source_func_t datafunc, int timeout) {
int error;
if (!ctx->stack->resolver_send_error) {
......@@ -225,13 +229,13 @@ typedef struct delayed_send {
static int on_delayed_send_do(delayed_send_t *ds) {
belle_sip_message("%s sending now", __FUNCTION__);
_resolver_start_query(ds->ctx, ds->datafunc, ds->timeout);
_resolver_send_query(ds->ctx, ds->datafunc, ds->timeout);
belle_sip_object_unref(ds->ctx);
belle_sip_free(ds);
return FALSE;
}
static int resolver_start_query(belle_sip_resolver_context_t *ctx, belle_sip_source_func_t datafunc, int timeout) {
static int resolver_send_query(belle_sip_resolver_context_t *ctx, belle_sip_source_func_t datafunc, int timeout) {
struct dns_hints *(*hints)() = &dns_hints_local;
struct dns_options *opts;
#ifndef HAVE_C99
......@@ -266,7 +270,25 @@ static int resolver_start_query(belle_sip_resolver_context_t *ctx, belle_sip_sou
belle_sip_message("%s DNS resolution delayed by %d ms", __FUNCTION__, ctx->stack->resolver_tx_delay);
return 0;
} else {
return _resolver_start_query(ctx, datafunc, timeout);
return _resolver_send_query(ctx, datafunc, timeout);
}
}
static int resolver_start_query(belle_sip_resolver_context_t *ctx) {
if (resolver_send_query(ctx,
(belle_sip_source_func_t)resolver_process_data,
belle_sip_stack_get_dns_timeout(ctx->stack)) < 0) {
belle_sip_object_unref(ctx);
return 0;
}
if ((ctx->done == FALSE) && (ctx->started == FALSE)) {
/* The resolver context must never be removed manually from the main loop */
belle_sip_main_loop_add_source(ctx->ml, (belle_sip_source_t *)ctx);
belle_sip_object_unref(ctx); /* The main loop has a ref on it */
ctx->started = TRUE;
return ctx->source.id;
} else {
return 0; /*resolution done synchronously*/
}
}
......@@ -307,26 +329,145 @@ struct addrinfo * belle_sip_ip_address_to_addrinfo(int family, const char *ipadd
static void belle_sip_resolver_context_destroy(belle_sip_resolver_context_t *ctx){
/* Do not free elements of ctx->ai_list with freeaddrinfo(). Let the caller do it, otherwise
it will not be able to use them after the resolver has been destroyed. */
if (ctx->name)
if (ctx->name != NULL) {
belle_sip_free(ctx->name);
if (ctx->R)
ctx->name = NULL;
}
if (ctx->R != NULL) {
dns_res_close(ctx->R);
if (ctx->hosts) {
ctx->R = NULL;
}
if (ctx->hosts != NULL) {
dns_hosts_close(ctx->hosts);
ctx->hosts = NULL;
}
if (ctx->resconf)
if (ctx->resconf != NULL) {
free(ctx->resconf);
ctx->resconf = NULL;
}
}
BELLE_SIP_DECLARE_NO_IMPLEMENTED_INTERFACES(belle_sip_resolver_context_t);
BELLE_SIP_INSTANCIATE_VPTR(belle_sip_resolver_context_t, belle_sip_source_t,belle_sip_resolver_context_destroy, NULL, NULL,FALSE);
unsigned long belle_sip_resolve(belle_sip_stack_t *stack, const char *name, int port, int family, belle_sip_resolver_callback_t cb , void *data, belle_sip_main_loop_t *ml) {
struct belle_sip_recursive_resolve_data {
belle_sip_resolver_context_t *ctx;
belle_sip_resolver_callback_t cb;
void *data;
char *name;
int port;
int family;
belle_sip_list_t *srv_list;
int next_srv;
struct addrinfo *ai_list;
};
static char * srv_prefix_from_transport(const char *transport) {
char *prefix = "";
if (strcasecmp(transport, "udp") == 0) {
prefix = "_sip._udp.";
} else if (strcasecmp(transport, "tcp") == 0) {
prefix = "_sip._tcp.";
} else if (strcasecmp(transport, "tls") == 0) {
prefix = "_sips._tcp.";
} else {
prefix = "_sip._udp.";
}
return prefix;
}
static void process_a_results(void *data, const char *name, struct addrinfo *ai_list);
static int start_a_query_from_srv_results(struct belle_sip_recursive_resolve_data *rec_data) {
if (rec_data->srv_list && belle_sip_list_size(rec_data->srv_list) > rec_data->next_srv) {
struct dns_srv *srv;
rec_data->next_srv++;
srv = belle_sip_list_nth_data(rec_data->srv_list, 0);
rec_data->ctx->cb = process_a_results;
rec_data->ctx->name = srv->target;
rec_data->ctx->port = srv->port;
rec_data->ctx->family = rec_data->family;
rec_data->ctx->type = (rec_data->family == AF_INET6) ? DNS_T_AAAA : DNS_T_A;
resolver_start_query(rec_data->ctx);
return 1;
}
return 0;
}
static void process_a_results(void *data, const char *name, struct addrinfo *ai_list) {
struct belle_sip_recursive_resolve_data *rec_data = (struct belle_sip_recursive_resolve_data *)data;
rec_data->ai_list = ai_list_append(rec_data->ai_list, ai_list);
if (!start_a_query_from_srv_results(rec_data)) {
/* All the SRV results have been queried, return the results */
(*rec_data->cb)(rec_data->data, rec_data->name, rec_data->ai_list);
rec_data->ctx->name = NULL;
if (rec_data->srv_list != NULL) {
belle_sip_list_for_each(rec_data->srv_list, belle_sip_free);
belle_sip_list_free(rec_data->srv_list);
rec_data->srv_list = NULL;
}
if (rec_data->name != NULL) {
belle_sip_free(rec_data->name);
}
belle_sip_free(rec_data);
} else {
rec_data->ctx->done = FALSE;
}
}
static void process_srv_results(void *data, const char *name, belle_sip_list_t *srv_list) {
struct belle_sip_recursive_resolve_data *rec_data = (struct belle_sip_recursive_resolve_data *)data;
rec_data->srv_list = srv_list;
rec_data->ctx->done = FALSE;
belle_sip_resolver_context_destroy(rec_data->ctx);
if (!start_a_query_from_srv_results(rec_data)) {
/* There was no SRV results, try to perform the A or AAAA directly. */
rec_data->ctx->cb = process_a_results;
rec_data->ctx->name = rec_data->name;
rec_data->ctx->port = rec_data->port;
rec_data->ctx->family = rec_data->family;
rec_data->ctx->type = (rec_data->family == AF_INET6) ? DNS_T_AAAA : DNS_T_A;
resolver_start_query(rec_data->ctx);
}
}
unsigned long belle_sip_resolve(belle_sip_stack_t *stack, const char *transport, const char *name, int port, int family, belle_sip_resolver_callback_t cb, void *data, belle_sip_main_loop_t *ml) {
struct addrinfo *res = belle_sip_ip_address_to_addrinfo(family, name, port);
if (res == NULL) {
/* Then perform asynchronous DNS query */
/* Then perform asynchronous DNS SRV query */
struct belle_sip_recursive_resolve_data *rec_data = belle_sip_malloc0(sizeof(struct belle_sip_recursive_resolve_data));
belle_sip_resolver_context_t *ctx = belle_sip_object_new(belle_sip_resolver_context_t);
ctx->stack = stack;
ctx->ml = ml;
ctx->cb_data = rec_data;
ctx->srv_cb = process_srv_results;
ctx->name = belle_sip_concat(srv_prefix_from_transport(transport), name, NULL);
ctx->type = DNS_T_SRV;
rec_data->ctx = ctx;
rec_data->cb = cb;
rec_data->data = data;
rec_data->name = belle_sip_strdup(name);
rec_data->port = port;
if (family == 0) family = AF_UNSPEC;
rec_data->family = family;
return resolver_start_query(ctx);
} else {
/* There is no resolve to be done */
cb(data, name, res);
return 0;
}
}
unsigned long belle_sip_resolve_a(belle_sip_stack_t *stack, const char *name, int port, int family, belle_sip_resolver_callback_t cb , void *data, belle_sip_main_loop_t *ml) {
struct addrinfo *res = belle_sip_ip_address_to_addrinfo(family, name, port);
if (res == NULL) {
/* Then perform asynchronous DNS A or AAAA query */
belle_sip_resolver_context_t *ctx = belle_sip_object_new(belle_sip_resolver_context_t);
ctx->stack = stack;
ctx->ml = ml;
ctx->cb_data = data;
ctx->cb = cb;
ctx->name = belle_sip_strdup(name);
......@@ -334,21 +475,9 @@ unsigned long belle_sip_resolve(belle_sip_stack_t *stack, const char *name, int
if (family == 0) family = AF_UNSPEC;
ctx->family = family;
ctx->type = (ctx->family == AF_INET6) ? DNS_T_AAAA : DNS_T_A;
if (resolver_start_query(ctx,
(belle_sip_source_func_t)resolver_process_data,
belle_sip_stack_get_dns_timeout(stack)) < 0) {
belle_sip_object_unref(ctx);
return 0;
}
if (ctx->done == FALSE) {
/* The resolver context must never be removed manually from the main loop */
belle_sip_main_loop_add_source(ml, (belle_sip_source_t *)ctx);
belle_sip_object_unref(ctx); /* The main loop has a ref on it */
return ctx->source.id;
} else {
return 0; /*resolution done synchronously*/
}
return resolver_start_query(ctx);
} else {
/* There is no resolve to be done */
cb(data, name, res);
return 0;
}
......@@ -356,35 +485,13 @@ unsigned long belle_sip_resolve(belle_sip_stack_t *stack, const char *name, int
unsigned long belle_sip_resolve_srv(belle_sip_stack_t *stack, const char *name, const char *transport, belle_sip_resolver_srv_callback_t cb, void *data, belle_sip_main_loop_t *ml) {
belle_sip_resolver_context_t *ctx = belle_sip_object_new(belle_sip_resolver_context_t);
char *prefix;
if (strcasecmp(transport, "udp") == 0) {
prefix = "_sip._udp.";
} else if (strcasecmp(transport, "tcp") == 0) {
prefix = "_sip._tcp.";
} else if (strcasecmp(transport, "tls") == 0) {
prefix = "_sips._tcp.";
} else {
prefix = "_sip._udp.";
}
ctx->stack = stack;
ctx->ml = ml;
ctx->cb_data = data;
ctx->srv_cb = cb;
ctx->name = belle_sip_concat(prefix, name, NULL);
ctx->name = belle_sip_concat(srv_prefix_from_transport(transport), name, NULL);
ctx->type = DNS_T_SRV;
if (resolver_start_query(ctx,
(belle_sip_source_func_t)resolver_process_data,
belle_sip_stack_get_dns_timeout(stack)) < 0) {
belle_sip_object_unref(ctx);
return 0;
}
if (ctx->done == FALSE) {
/* The resolver context must never be removed manually from the main loop */
belle_sip_main_loop_add_source(ml, (belle_sip_source_t *)ctx);
belle_sip_object_unref(ctx); /* The main loop has a ref on it */
return ctx->source.id;
} else {
return 0; /*resolution done synchronously*/
}
return resolver_start_query(ctx);
}
void belle_sip_resolve_cancel(belle_sip_main_loop_t *ml, unsigned long id){
......
......@@ -45,6 +45,7 @@ typedef void (*belle_sip_resolver_srv_callback_t)(void *data, const char *name,
struct belle_sip_resolver_context{
belle_sip_source_t source;
belle_sip_stack_t *stack;
belle_sip_main_loop_t *ml;
belle_sip_resolver_callback_t cb;
belle_sip_resolver_srv_callback_t srv_cb;
void *cb_data;
......@@ -58,6 +59,7 @@ struct belle_sip_resolver_context{
belle_sip_list_t *srv_list;
int family;
uint8_t cancelled;
uint8_t started;
uint8_t done;
};
......@@ -65,7 +67,8 @@ BELLE_SIP_BEGIN_DECLS
int belle_sip_addrinfo_to_ip(const struct addrinfo *ai, char *ip, size_t ip_size, int *port);
BELLESIP_INTERNAL_EXPORT struct addrinfo * belle_sip_ip_address_to_addrinfo(int family, const char *ipaddress, int port);
BELLESIP_INTERNAL_EXPORT unsigned long belle_sip_resolve(belle_sip_stack_t *stack, const char *name, int port, int family, belle_sip_resolver_callback_t cb, void *data, belle_sip_main_loop_t *ml);
BELLESIP_INTERNAL_EXPORT unsigned long belle_sip_resolve(belle_sip_stack_t *stack, const char *name, const char *transport, int port, int family, belle_sip_resolver_callback_t cb, void *data, belle_sip_main_loop_t *ml);
BELLESIP_INTERNAL_EXPORT unsigned long belle_sip_resolve_a(belle_sip_stack_t *stack, const char *name, int port, int family, belle_sip_resolver_callback_t cb, void *data, belle_sip_main_loop_t *ml);
BELLESIP_INTERNAL_EXPORT unsigned long belle_sip_resolve_srv(belle_sip_stack_t *stack, const char *name, const char *transport, belle_sip_resolver_srv_callback_t cb, void *data, belle_sip_main_loop_t *ml);
void belle_sip_resolve_cancel(belle_sip_main_loop_t *ml, unsigned long id);
......
......@@ -600,7 +600,7 @@ static void channel_res_done(void *data, const char *name, struct addrinfo *ai_l
void belle_sip_channel_resolve(belle_sip_channel_t *obj){
channel_set_state(obj,BELLE_SIP_CHANNEL_RES_IN_PROGRESS);
obj->resolver_id=belle_sip_resolve(obj->stack, obj->peer_name, obj->peer_port, obj->lp->ai_family, channel_res_done, obj, obj->stack->ml);
obj->resolver_id=belle_sip_resolve_a(obj->stack, obj->peer_name, obj->peer_port, obj->lp->ai_family, channel_res_done, obj, obj->stack->ml);
return ;
}
......
......@@ -63,11 +63,6 @@ static endpoint_t* create_endpoint() {
return endpoint;
}
static void free_srv_result(void *ptr) {
struct dns_srv *srv = (struct dns_srv *)ptr;
belle_sip_free(srv);
}
static void reset_endpoint(endpoint_t *endpoint) {
endpoint->resolver_id = 0;
endpoint->resolve_done = 0;
......@@ -77,7 +72,7 @@ static void reset_endpoint(endpoint_t *endpoint) {
endpoint->ai_list = NULL;
}
if (endpoint->srv_list != NULL) {
belle_sip_list_for_each(endpoint->srv_list, free_srv_result);
belle_sip_list_for_each(endpoint->srv_list, belle_sip_free);
belle_sip_list_free(endpoint->srv_list);
endpoint->srv_list = NULL;
}
......@@ -120,7 +115,7 @@ static void ipv4_a_query(void) {
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
timeout = belle_sip_stack_get_dns_timeout(client->stack);
client->resolver_id = belle_sip_resolve(client->stack, IPV4_SIP_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
client->resolver_id = belle_sip_resolve_a(client->stack, IPV4_SIP_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, timeout));
CU_ASSERT_PTR_NOT_EQUAL(client->ai_list, NULL);
......@@ -144,7 +139,7 @@ static void ipv4_a_query_no_result(void) {
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
timeout = belle_sip_stack_get_dns_timeout(client->stack);
client->resolver_id = belle_sip_resolve(client->stack, IPV4_SIP_BAD_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
client->resolver_id = belle_sip_resolve_a(client->stack, IPV4_SIP_BAD_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, timeout));
CU_ASSERT_PTR_EQUAL(client->ai_list, NULL);
......@@ -158,7 +153,7 @@ static void ipv4_a_query_send_failure(void) {
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
belle_sip_stack_set_resolver_send_error(client->stack, -1);
client->resolver_id = belle_sip_resolve(client->stack, IPV4_SIP_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
client->resolver_id = belle_sip_resolve_a(client->stack, IPV4_SIP_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_EQUAL(client->resolver_id, 0);
belle_sip_stack_set_resolver_send_error(client->stack, 0);
......@@ -172,7 +167,7 @@ static void ipv4_a_query_timeout(void) {
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
belle_sip_stack_set_dns_timeout(client->stack, 0);
client->resolver_id = belle_sip_resolve(client->stack, "toto.com", SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
client->resolver_id = belle_sip_resolve_a(client->stack, "toto.com", SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, 200));
CU_ASSERT_PTR_EQUAL(client->ai_list, NULL);
......@@ -187,7 +182,7 @@ static void ipv4_a_query_multiple_results(void) {
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
timeout = belle_sip_stack_get_dns_timeout(client->stack);
client->resolver_id = belle_sip_resolve(client->stack, IPV4_MULTIRES_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
client->resolver_id = belle_sip_resolve_a(client->stack, IPV4_MULTIRES_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, timeout));
CU_ASSERT_PTR_NOT_EQUAL(client->ai_list, NULL);
......@@ -206,7 +201,7 @@ static void ipv6_aaaa_query(void) {
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
timeout = belle_sip_stack_get_dns_timeout(client->stack);
client->resolver_id = belle_sip_resolve(client->stack, IPV6_SIP_DOMAIN, SIP_PORT, AF_INET6, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
client->resolver_id = belle_sip_resolve_a(client->stack, IPV6_SIP_DOMAIN, SIP_PORT, AF_INET6, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, timeout));
CU_ASSERT_PTR_NOT_EQUAL(client->ai_list, NULL);
......@@ -247,6 +242,47 @@ static void srv_query(void) {
destroy_endpoint(client);
}
/* Successful SRV + A or AAAA queries */
static void srv_a_query(void) {
int timeout;
endpoint_t *client = create_endpoint();
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
timeout = belle_sip_stack_get_dns_timeout(client->stack);
client->resolver_id = belle_sip_resolve(client->stack, "udp", SRV_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, timeout));
CU_ASSERT_PTR_NOT_EQUAL(client->ai_list, NULL);
destroy_endpoint(client);
}
/* Successful SRV query with no result + A query */
static void srv_a_query_no_srv_result(void) {
struct addrinfo *ai;
int timeout;
endpoint_t *client = create_endpoint();
CU_ASSERT_PTR_NOT_NULL_FATAL(client);
timeout = belle_sip_stack_get_dns_timeout(client->stack);
client->resolver_id = belle_sip_resolve(client->stack, "udp", IPV4_SIP_DOMAIN, SIP_PORT, AF_INET, a_resolve_done, client, belle_sip_stack_get_main_loop(client->stack));
CU_ASSERT_NOT_EQUAL(client->resolver_id, 0);
CU_ASSERT_TRUE(wait_for(client->stack, &client->resolve_done, 1, timeout));
CU_ASSERT_PTR_NOT_EQUAL(client->ai_list, NULL);
if (client->ai_list) {
struct sockaddr_in *sock_in = (struct sockaddr_in *)client->ai_list->ai_addr;
CU_ASSERT_EQUAL(ntohs(sock_in->sin_port), SIP_PORT);
ai = belle_sip_ip_address_to_addrinfo(AF_INET, IPV4_SIP_IP, SIP_PORT);
if (ai) {
CU_ASSERT_EQUAL(sock_in->sin_addr.s_addr, ((struct sockaddr_in *)ai->ai_addr)->sin_addr.s_addr);
freeaddrinfo(ai);
}
}
destroy_endpoint(client);
}
test_t resolver_tests[] = {
{ "A query (IPv4)", ipv4_a_query },
......@@ -256,6 +292,8 @@ test_t resolver_tests[] = {
{ "A query (IPv4) with multiple results", ipv4_a_query_multiple_results },
{ "AAAA query (IPv6)", ipv6_aaaa_query },
{ "SRV query", srv_query },
{ "SRV + A query", srv_a_query },
{ "SRV + A query with no SRV result", srv_a_query_no_srv_result },
};
test_suite_t resolver_test_suite = {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment