curl/tests/server/dnsd.c
Stefan Eissing 86f1e5b3f6
test dnsd: implement delayed responses
Add "Delay-A: ms", "Delay-AAAA: ms" and "Delay-HTTPS: ms" to the test
dnsd config and implement delayed response handling.

Add test_21_09 and test_21_10 to check that delayed responses connect
using the undelayed address family.

Closes #21299
2026-04-14 16:49:07 +02:00

853 lines
22 KiB
C

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "first.h"
static int dnsd_wrotepidfile = 0;
static int dnsd_wroteportfile = 0;
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#ifdef __AMIGA__
#error building dnsd on AMIGA os is unsupported
#endif
static unsigned short get16bit(const unsigned char **pkt, size_t *size)
{
const unsigned char *p = *pkt;
(*pkt) += 2;
*size -= 2;
return (unsigned short)((p[0] << 8) | p[1]);
}
static char name[256];
static int qname(const unsigned char **pkt, size_t *size)
{
unsigned char length;
int o = 0;
const unsigned char *p = *pkt;
do {
int i;
length = *p++;
if(*size < length)
/* too long */
return 1;
if(length && o)
name[o++] = '.';
for(i = 0; i < length; i++) {
name[o++] = *p++;
}
} while(length);
*size -= (p - *pkt);
*pkt = p;
name[o++] = '\0';
return 0;
}
#define QTYPE_A 1
#define QTYPE_AAAA 28
#define QTYPE_HTTPS 0x41
static const char *type2string(unsigned short qtype)
{
switch(qtype) {
case QTYPE_A:
return "A";
case QTYPE_AAAA:
return "AAAA";
case QTYPE_HTTPS:
return "HTTPS";
}
return "<unknown>";
}
/*
* Handle initial connection protocol.
*
* Return query (qname + type + class), type and id.
*/
static int store_incoming(int qid, const unsigned char *data, size_t size,
unsigned char *qbuf, size_t qbuflen, size_t *qlen,
unsigned short *qtype, unsigned short *idp)
{
FILE *server;
char dumpfile[256];
#if 0
size_t i;
#endif
unsigned short qd;
const unsigned char *qptr;
size_t qsize;
*qlen = 0;
*qtype = 0;
*idp = 0;
snprintf(dumpfile, sizeof(dumpfile), "%s/dnsd.input", logdir);
/* Open request dump file. */
server = curlx_fopen(dumpfile, "ab");
if(!server) {
char errbuf[STRERROR_LEN];
int error = errno;
logmsg("fopen() failed with error (%d) %s",
error, curlx_strerror(error, errbuf, sizeof(errbuf)));
logmsg("Error opening file '%s'", dumpfile);
return -1;
}
/*
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
*/
*idp = get16bit(&data, &size);
data += 2; /* skip the next 16 bits */
size -= 2;
#if 0
fprintf(server, "QR: %x\n", (*idp & 0x8000) > 15);
fprintf(server, "OPCODE: %x\n", (*idp & 0x7800) >> 11);
fprintf(server, "TC: %x\n", (*idp & 0x200) >> 9);
fprintf(server, "RD: %x\n", (*idp & 0x100) >> 8);
fprintf(server, "Z: %x\n", (*idp & 0x70) >> 4);
fprintf(server, "RCODE: %x\n", (*idp & 0x0f));
#endif
(void)get16bit(&data, &size);
data += 6; /* skip ANCOUNT, NSCOUNT and ARCOUNT */
size -= 6;
/* store pointer and size at the QD point */
qsize = size;
qptr = data;
if(!qname(&data, &size)) {
qd = get16bit(&data, &size);
fprintf(server, "QNAME %s QTYPE %s\n", name, type2string(qd));
*qtype = qd;
logmsg("[%d] Question for '%s' type %x / %s",
qid, name, qd, type2string(qd));
(void)get16bit(&data, &size);
*qlen = qsize - size; /* total size of the query */
if(*qlen > qbuflen) {
logmsg("dnsd: query too large: %lu > %lu",
(unsigned long)*qlen, (unsigned long)qbuflen);
curlx_fclose(server);
return -1;
}
memcpy(qbuf, qptr, *qlen);
}
else
logmsg("Bad input qname");
#if 0
for(i = 0; i < size; i++) {
fprintf(server, "%02d", (unsigned int)data[i]);
}
fprintf(server, "\n");
#endif
curlx_fclose(server);
return 0;
}
static void add_answer(unsigned char *bytes, size_t *w,
const unsigned char *a, size_t alen,
unsigned short qtype)
{
size_t i = *w;
/* add answer */
bytes[i++] = 0xc0;
bytes[i++] = 0x0c; /* points to the query at this fixed packet index */
/* QTYPE */
bytes[i++] = (unsigned char)(qtype >> 8);
bytes[i++] = (unsigned char)(qtype & 0xff);
/* QCLASS IN */
bytes[i++] = 0x00;
bytes[i++] = 0x01;
/* TTL, Time to live: 2580 (43 minutes) */
bytes[i++] = 0x00;
bytes[i++] = 0x00;
bytes[i++] = 0x0a;
bytes[i++] = 0x14;
/* QTYPE size */
bytes[i++] = (unsigned char)(alen >> 8);
bytes[i++] = (unsigned char)(alen & 0xff);
memcpy(&bytes[i], a, alen);
i += alen;
*w = i;
}
#ifdef _WIN32
#define SENDTO3 int
#else
#define SENDTO3 size_t
#endif
#define INSTRUCTIONS "dnsd.cmd"
#define MAX_ALPN 5
static unsigned char ipv4_pref[4];
static unsigned char ipv6_pref[16];
static unsigned char alpn_pref[MAX_ALPN];
static int alpn_count;
static unsigned char ancount_a;
static unsigned char ancount_aaaa;
static timediff_t a_delay_ms;
static timediff_t aaaa_delay_ms;
static timediff_t https_delay_ms;
static int query_id = -1;
struct resp {
struct resp *next;
int qid;
struct curltime send_ts;
struct sockaddr addr;
curl_socklen_t addrlen;
char body[256];
size_t blen;
};
static struct resp *resp_queue;
static CURLcode send_resp(curl_socket_t sock, struct resp *resp)
{
ssize_t rc;
sending:
rc = sendto(sock, (const void *)resp->body, (SENDTO3)resp->blen, 0,
&resp->addr, resp->addrlen);
if((rc < 0) && (SOCKERRNO == SOCKEINTR))
goto sending;
if(rc != (ssize_t)resp->blen) {
logmsg("failed sending %d bytes, errno=%d\n", (int)resp->blen, SOCKERRNO);
return CURLE_SEND_ERROR;
}
logmsg("[%d] sent response", resp->qid);
return CURLE_OK;
}
static void queue_resp(struct resp *resp)
{
struct resp **panchor = &resp_queue;
while(*panchor) {
timediff_t ms = curlx_ptimediff_ms(&(*panchor)->send_ts, &resp->send_ts);
if(ms > 0) /* resp is to be sent before *panchor */
break;
panchor = &(*panchor)->next;
}
resp->next = *panchor;
*panchor = resp;
}
static timediff_t send_resp_queue(curl_socket_t sock)
{
struct resp **panchor = &resp_queue;
struct curltime now = curlx_now();
timediff_t timeout_ms = 0;
while(*panchor) {
struct resp *resp = *panchor;
timediff_t ms = curlx_ptimediff_ms(&resp->send_ts, &now);
if(ms > 0) {
timeout_ms = ms;
break;
}
*panchor = resp->next;
send_resp(sock, resp);
curlx_free(resp);
}
return timeout_ms;
}
static void clear_resp_queue(void)
{
while(resp_queue) {
struct resp *resp = resp_queue;
resp_queue = resp->next;
curlx_free(resp);
}
}
/* this is an answer to a question */
static struct resp *
create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen,
const unsigned char *qbuf, size_t qlen,
unsigned short qtype, unsigned short id)
{
struct resp *resp;
size_t i;
int a;
timediff_t delay_ms = 0;
char addrbuf[128]; /* IP address buffer */
unsigned char bytes[256] = {
0x80, 0xea, /* ID, overwrite */
0x81, 0x80,
/*
Flags: 0x8180 Standard query response, No error
1... .... .... .... = Response: Message is a response
.000 0... .... .... = Opcode: Standard query (0)
.... .0.. .... .... = Authoritative: Server is not an authority for
domain
.... ..0. .... .... = Truncated: Message is not truncated
.... ...1 .... .... = Recursion desired: Do query recursively
.... .... 1... .... = Recursion available: Server can do recursive
queries
.... .... .0.. .... = Z: reserved (0)
.... .... ..0. .... = Answer authenticated: Answer/authority portion
was not authenticated by the server
.... .... ...0 .... = Non-authenticated data: Unacceptable
.... .... .... 0000 = Reply code: No error (0)
*/
0x0, 0x1, /* QDCOUNT a single question */
0x0, 0x0, /* ANCOUNT number of answers */
0x0, 0x0, /* NSCOUNT */
0x0, 0x0 /* ARCOUNT */
};
resp = curlx_calloc(1, sizeof(*resp));
if(!resp)
return NULL;
resp->qid = qid;
/* on some platforms `curl_socklen_t` is an `int`. Casting might
* wrap this, but then it still has to fit our record size. */
if((size_t)addrlen > sizeof(resp->addr)) {
logmsg("unable to handle addrlen of %zu", (size_t)addrlen);
curlx_free(resp);
return NULL;
}
memcpy(&resp->addr, CURL_UNCONST(addr), addrlen);
resp->addrlen = addrlen;
bytes[0] = (unsigned char)(id >> 8);
bytes[1] = (unsigned char)(id & 0xff);
if(qlen > (sizeof(bytes) - 12)) {
logmsg("unable to handle query of length %zu", qlen);
curlx_free(resp);
return NULL;
}
/* append query, includes QTYPE and QCLASS */
memcpy(&bytes[12], qbuf, qlen);
i = 12 + qlen;
switch(qtype) {
case QTYPE_A:
bytes[7] = ancount_a;
for(a = 0; a < ancount_a; a++) {
const unsigned char *store = ipv4_pref;
add_answer(bytes, &i, store, sizeof(ipv4_pref), QTYPE_A);
logmsg("[%d] response A (%x) '%s'", qid, QTYPE_A,
curlx_inet_ntop(AF_INET, store, addrbuf, sizeof(addrbuf)));
}
if(!ancount_a)
logmsg("[%d] response A empty", qid);
delay_ms = a_delay_ms;
break;
case QTYPE_AAAA:
bytes[7] = ancount_aaaa;
for(a = 0; a < ancount_aaaa; a++) {
const unsigned char *store = ipv6_pref;
add_answer(bytes, &i, store, sizeof(ipv6_pref), QTYPE_AAAA);
logmsg("[%d] response AAAA (%x) '%s'", qid, QTYPE_AAAA,
curlx_inet_ntop(AF_INET6, store, addrbuf, sizeof(addrbuf)));
}
if(!ancount_aaaa)
logmsg("[%d] response AAAA empty", qid);
delay_ms = aaaa_delay_ms;
break;
case QTYPE_HTTPS:
logmsg("[%d] response HTTPS (empty, so far)", qid);
bytes[7] = 0; /* no answer so far */
delay_ms = https_delay_ms;
break;
}
memcpy(&resp->body, bytes, i);
resp->blen = i;
resp->send_ts = curlx_now();
if(delay_ms > 0) {
int usec = (int)((delay_ms % 1000) * 1000);
resp->send_ts.tv_sec += (time_t)(delay_ms / 1000);
resp->send_ts.tv_usec += usec;
if(resp->send_ts.tv_usec >= 1000000) {
resp->send_ts.tv_sec++;
resp->send_ts.tv_usec -= 1000000;
}
}
return resp;
}
static void read_instructions(void)
{
char file[256];
FILE *f;
/* reset defaults */
a_delay_ms = aaaa_delay_ms = https_delay_ms = 0;
snprintf(file, sizeof(file), "%s/" INSTRUCTIONS, logdir);
f = curlx_fopen(file, FOPEN_READTEXT);
if(f) {
char buf[256];
ancount_aaaa = ancount_a = 0;
alpn_count = 0;
while(fgets(buf, sizeof(buf), f)) {
char *p = strchr(buf, '\n');
if(p) {
int rc;
*p = 0;
if(!strncmp("A: ", buf, 3)) {
rc = curlx_inet_pton(AF_INET, &buf[3], ipv4_pref);
ancount_a = (rc == 1);
}
else if(!strncmp("AAAA: ", buf, 6)) {
char *p6 = &buf[6];
if(*p6 == '[') {
char *pt = strchr(p6, ']');
if(pt)
*pt = 0;
p6++;
}
rc = curlx_inet_pton(AF_INET6, p6, ipv6_pref);
ancount_aaaa = (rc == 1);
}
else if(!strncmp("ALPN: ", buf, 6)) {
char *ap = &buf[6];
rc = 0;
while(*ap) {
if('h' == *ap) {
ap++;
if(*ap >= '1' && *ap <= '3') {
if(alpn_count < MAX_ALPN)
alpn_pref[alpn_count++] = *ap;
}
else
break;
}
else
break;
}
}
else if(!strncmp("Delay-A: ", buf, 9)) {
curl_off_t ms;
const char *pms = &buf[9];
rc = 0;
if(!curlx_str_number(&pms, &ms, 100000)) {
a_delay_ms = (timediff_t)ms;
rc = 1;
}
}
else if(!strncmp("Delay-AAAA: ", buf, 12)) {
curl_off_t ms;
const char *pms = &buf[12];
rc = 0;
if(!curlx_str_number(&pms, &ms, 100000)) {
aaaa_delay_ms = (timediff_t)ms;
rc = 1;
}
}
else if(!strncmp("Delay-HTTPS: ", buf, 13)) {
curl_off_t ms;
const char *pms = &buf[13];
rc = 0;
if(!curlx_str_number(&pms, &ms, 100000)) {
https_delay_ms = (timediff_t)ms;
rc = 1;
}
}
else {
/* accept empty line */
rc = buf[0] ? 0 : 1;
}
if(rc != 1) {
logmsg("Bad line in %s: '%s'\n", file, buf);
}
}
}
curlx_fclose(f);
}
else
logmsg("Error opening file '%s'", file);
}
static int test_dnsd(int argc, const char **argv)
{
srvr_sockaddr_union_t me;
ssize_t n = 0;
int arg = 1;
unsigned short port = 9123; /* UDP */
curl_socket_t sock = CURL_SOCKET_BAD;
int flag;
int rc;
int error;
char errbuf[STRERROR_LEN];
int result = 0;
struct resp *resp;
pidname = ".dnsd.pid";
serverlogfile = "log/dnsd.log";
serverlogslocked = 0;
while(argc > arg) {
const char *opt;
curl_off_t num;
if(!strcmp("--verbose", argv[arg])) {
arg++;
/* nothing yet */
}
else if(!strcmp("--version", argv[arg])) {
printf("dnsd IPv4%s\n",
#ifdef USE_IPV6
"/IPv6"
#else
""
#endif
);
return 0;
}
else if(!strcmp("--pidfile", argv[arg])) {
arg++;
if(argc > arg)
pidname = argv[arg++];
}
else if(!strcmp("--portfile", argv[arg])) {
arg++;
if(argc > arg)
portname = argv[arg++];
}
else if(!strcmp("--logfile", argv[arg])) {
arg++;
if(argc > arg)
serverlogfile = argv[arg++];
}
else if(!strcmp("--logdir", argv[arg])) {
arg++;
if(argc > arg)
logdir = argv[arg++];
}
else if(!strcmp("--ipv4", argv[arg])) {
#ifdef USE_IPV6
ipv_inuse = "IPv4";
use_ipv6 = FALSE;
#endif
arg++;
}
else if(!strcmp("--ipv6", argv[arg])) {
#ifdef USE_IPV6
ipv_inuse = "IPv6";
use_ipv6 = TRUE;
#endif
arg++;
}
else if(!strcmp("--port", argv[arg])) {
arg++;
if(argc > arg) {
opt = argv[arg];
if(!curlx_str_number(&opt, &num, 0xffff))
port = (unsigned short)num;
arg++;
}
}
else {
if(argv[arg])
fprintf(stderr, "unknown option: %s\n", argv[arg]);
puts("Usage: dnsd [option]\n"
" --version\n"
" --logfile [file]\n"
" --logdir [directory]\n"
" --pidfile [file]\n"
" --portfile [file]\n"
" --ipv4\n"
" --ipv6\n"
" --port [port]\n");
return 0;
}
}
snprintf(loglockfile, sizeof(loglockfile), "%s/%s/dnsd-%s.lock",
logdir, SERVERLOGS_LOCKDIR, ipv_inuse);
#ifdef USE_IPV6
if(!use_ipv6)
#endif
sock = socket(AF_INET, SOCK_DGRAM, 0);
#ifdef USE_IPV6
else
sock = socket(AF_INET6, SOCK_DGRAM, 0);
#endif
if(sock == CURL_SOCKET_BAD) {
error = SOCKERRNO;
logmsg("Error creating socket (%d) %s",
error, curlx_strerror(error, errbuf, sizeof(errbuf)));
result = 1;
goto dnsd_cleanup;
}
flag = 1;
if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&flag, sizeof(flag))) {
error = SOCKERRNO;
logmsg("setsockopt(SO_REUSEADDR) failed with error (%d) %s",
error, curlx_strerror(error, errbuf, sizeof(errbuf)));
result = 1;
goto dnsd_cleanup;
}
#ifdef USE_IPV6
if(!use_ipv6) {
#endif
memset(&me.sa4, 0, sizeof(me.sa4));
me.sa4.sin_family = AF_INET;
me.sa4.sin_addr.s_addr = INADDR_ANY;
me.sa4.sin_port = htons(port);
rc = bind(sock, &me.sa, sizeof(me.sa4));
#ifdef USE_IPV6
}
else {
memset(&me.sa6, 0, sizeof(me.sa6));
me.sa6.sin6_family = AF_INET6;
me.sa6.sin6_addr = in6addr_any;
me.sa6.sin6_port = htons(port);
rc = bind(sock, &me.sa, sizeof(me.sa6));
}
#endif /* USE_IPV6 */
if(rc) {
error = SOCKERRNO;
logmsg("Error binding socket on port %hu (%d) %s", port,
error, curlx_strerror(error, errbuf, sizeof(errbuf)));
result = 1;
goto dnsd_cleanup;
}
if(!port) {
/* The system was supposed to choose a port number, figure out which
port we actually got and update the listener port value with it. */
curl_socklen_t la_size;
srvr_sockaddr_union_t localaddr;
memset(&localaddr, 0, sizeof(localaddr));
#ifdef USE_IPV6
if(!use_ipv6)
#endif
la_size = sizeof(localaddr.sa4);
#ifdef USE_IPV6
else
la_size = sizeof(localaddr.sa6);
#endif
if(getsockname(sock, &localaddr.sa, &la_size) < 0) {
error = SOCKERRNO;
logmsg("getsockname() failed with error (%d) %s",
error, curlx_strerror(error, errbuf, sizeof(errbuf)));
sclose(sock);
goto dnsd_cleanup;
}
switch(localaddr.sa.sa_family) {
case AF_INET:
port = ntohs(localaddr.sa4.sin_port);
break;
#ifdef USE_IPV6
case AF_INET6:
port = ntohs(localaddr.sa6.sin6_port);
break;
#endif
default:
break;
}
if(!port) {
/* Real failure, listener port shall not be zero beyond this point. */
logmsg("Apparently getsockname() succeeded, with listener port zero.");
logmsg("A valid reason for this failure is a binary built without");
logmsg("proper network library linkage. This might not be the only");
logmsg("reason, but double check it before anything else.");
result = 2;
goto dnsd_cleanup;
}
}
dnsd_wrotepidfile = write_pidfile(pidname);
if(!dnsd_wrotepidfile) {
result = 1;
goto dnsd_cleanup;
}
if(portname) {
dnsd_wroteportfile = write_portfile(portname, port);
if(!dnsd_wroteportfile) {
result = 1;
goto dnsd_cleanup;
}
}
logmsg("Running %s version on port UDP/%d", ipv_inuse, (int)port);
curlx_nonblock(sock, TRUE);
for(;;) {
unsigned short id = 0;
unsigned char inbuffer[1500];
srvr_sockaddr_union_t from;
curl_socklen_t fromlen;
unsigned char qbuf[256]; /* query storage */
size_t qlen = 0; /* query size */
unsigned short qtype = 0;
timediff_t timeout_ms = 0;
fromlen = sizeof(from);
#ifdef USE_IPV6
if(!use_ipv6)
#endif
fromlen = sizeof(from.sa4);
#ifdef USE_IPV6
else
fromlen = sizeof(from.sa6);
#endif
timeout_ms = send_resp_queue(sock);
{
fd_set readfds;
struct timeval tv;
int maxfd = (int)sock;
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
if(!timeout_ms || (timeout_ms > 100))
timeout_ms = 100;
rc = select(maxfd + 1, &readfds, NULL, NULL,
curlx_mstotv(&tv, timeout_ms));
if(rc == -1) {
logmsg("error %d returned by select()", SOCKERRNO);
}
else if(!rc) { /* timeout */
logmsg("select timeout, run again");
continue;
}
}
n = (ssize_t)recvfrom(sock, (char *)inbuffer, sizeof(inbuffer), 0,
&from.sa, &fromlen);
if(got_exit_signal)
break;
if(n < 0) {
logmsg("recvfrom");
result = 3;
break;
}
/* read once per incoming query, which is probably more than one
per test case */
read_instructions();
++query_id;
store_incoming(query_id, inbuffer, n,
qbuf, sizeof(qbuf), &qlen, &qtype, &id);
set_advisor_read_lock(loglockfile);
serverlogslocked = 1;
resp = create_resp(query_id, &from.sa, fromlen, qbuf,
qlen, qtype, id);
if(!resp)
logmsg("error creating response");
else
queue_resp(resp);
if(got_exit_signal)
break;
if(serverlogslocked) {
serverlogslocked = 0;
clear_advisor_read_lock(loglockfile);
}
}
dnsd_cleanup:
#if 0
if((peer != sock) && (peer != CURL_SOCKET_BAD))
sclose(peer);
#endif
if(sock != CURL_SOCKET_BAD)
sclose(sock);
if(got_exit_signal)
logmsg("signalled to die");
if(dnsd_wrotepidfile)
unlink(pidname);
if(dnsd_wroteportfile)
unlink(portname);
if(serverlogslocked) {
serverlogslocked = 0;
clear_advisor_read_lock(loglockfile);
}
clear_resp_queue();
restore_signal_handlers(true);
if(got_exit_signal) {
logmsg("========> %s dnsd (port: %d pid: %ld) exits with signal (%d)",
ipv_inuse, (int)port, (long)our_getpid(), exit_signal);
/*
* To properly set the return status of the process we
* must raise the same signal SIGINT or SIGTERM that we
* caught and let the old handler take care of it.
*/
raise(exit_signal);
}
logmsg("========> dnsd quits");
return result;
}