From 50d1b3379a818df00c6382d68e2e39977cc96cd6 Mon Sep 17 00:00:00 2001 From: Anders Bakken Date: Tue, 30 Jan 2018 16:33:51 -0800 Subject: [PATCH] CURLOPT_RESOLVE: Add support for multiple IP addresses per entry This enables users to preresolve but still take advantage of happy eyeballs and trying multiple addresses if some are not connecting. Ref: https://github.com/curl/curl/pull/2260 --- docs/cmdline-opts/resolve.d | 4 +- docs/libcurl/opts/CURLOPT_RESOLVE.3 | 12 +- lib/connect.c | 16 +-- lib/connect.h | 5 + lib/hostip.c | 116 ++++++++++++---- tests/data/Makefile.inc | 2 +- tests/data/test1607 | 26 ++++ tests/unit/Makefile.inc | 5 +- tests/unit/unit1607.c | 203 ++++++++++++++++++++++++++++ 9 files changed, 347 insertions(+), 42 deletions(-) create mode 100644 tests/data/test1607 create mode 100644 tests/unit/unit1607.c diff --git a/docs/cmdline-opts/resolve.d b/docs/cmdline-opts/resolve.d index 91539b8e9..9e1457b5e 100644 --- a/docs/cmdline-opts/resolve.d +++ b/docs/cmdline-opts/resolve.d @@ -1,5 +1,5 @@ Long: resolve -Arg: +Arg: Help: Resolve the host+port to this address Added: 7.21.3 --- @@ -16,4 +16,6 @@ is set to make curl use another IP version. Support for providing the IP address within [brackets] was added in 7.57.0. +Support for providing multiple IP addresses per entry was added in 7.59.0. + This option can be used many times to add many host names to resolve. diff --git a/docs/libcurl/opts/CURLOPT_RESOLVE.3 b/docs/libcurl/opts/CURLOPT_RESOLVE.3 index c22f7b006..b4d79cf3b 100644 --- a/docs/libcurl/opts/CURLOPT_RESOLVE.3 +++ b/docs/libcurl/opts/CURLOPT_RESOLVE.3 @@ -37,10 +37,12 @@ list of \fBstruct curl_slist\fP structs properly filled in. Use to clean up an entire list. Each single name resolve string should be written using the format -HOST:PORT:ADDRESS where HOST is the name libcurl will try to resolve, PORT is -the port number of the service where libcurl wants to connect to the HOST and -ADDRESS is the numerical IP address. If libcurl is built to support IPv6, -ADDRESS can of course be either IPv4 or IPv6 style addressing. +HOST:PORT:ADDRESS[,ADDRESS]... where HOST is the name libcurl will try +to resolve, PORT is the port number of the service where libcurl wants +to connect to the HOST and ADDRESS is one or more numerical IP +addresses. If you specify multiple ip addresses they need to be +separated by comma. If libcurl is built to support IPv6, each of the +ADDRESS entries can of course be either IPv4 or IPv6 style addressing. This option effectively pre-populates the DNS cache with entries for the host+port pair so redirects and everything that operations against the @@ -57,6 +59,8 @@ by including a string in the linked list that uses the format and port number must exactly match what was already added previously. Support for providing the ADDRESS within [brackets] was added in 7.57.0. + +Support for providing multiple IP addresses per entry was added in 7.59.0. .SH DEFAULT NULL .SH PROTOCOLS diff --git a/lib/connect.c b/lib/connect.c index d56cf2f16..c3add43cc 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -619,8 +619,8 @@ void Curl_persistconninfo(struct connectdata *conn) /* retrieves ip address and port from a sockaddr structure. note it calls Curl_inet_ntop which sets errno on fail, not SOCKERRNO. */ -static bool getaddressinfo(struct sockaddr *sa, char *addr, - long *port) +bool Curl_getaddressinfo(struct sockaddr *sa, char *addr, + long *port) { unsigned short us_port; struct sockaddr_in *si = NULL; @@ -700,16 +700,16 @@ void Curl_updateconninfo(struct connectdata *conn, curl_socket_t sockfd) return; } - if(!getaddressinfo((struct sockaddr*)&ssrem, - conn->primary_ip, &conn->primary_port)) { + if(!Curl_getaddressinfo((struct sockaddr*)&ssrem, + conn->primary_ip, &conn->primary_port)) { failf(data, "ssrem inet_ntop() failed with errno %d: %s", errno, Curl_strerror(conn, errno)); return; } memcpy(conn->ip_addr_str, conn->primary_ip, MAX_IPADR_LEN); - if(!getaddressinfo((struct sockaddr*)&ssloc, - conn->local_ip, &conn->local_port)) { + if(!Curl_getaddressinfo((struct sockaddr*)&ssloc, + conn->local_ip, &conn->local_port)) { failf(data, "ssloc inet_ntop() failed with errno %d: %s", errno, Curl_strerror(conn, errno)); return; @@ -1005,8 +1005,8 @@ static CURLcode singleipconnect(struct connectdata *conn, return CURLE_OK; /* store remote address and port used in this connection attempt */ - if(!getaddressinfo((struct sockaddr*)&addr.sa_addr, - ipaddress, &port)) { + if(!Curl_getaddressinfo((struct sockaddr*)&addr.sa_addr, + ipaddress, &port)) { /* malformed address or bug in inet_ntop, try next address */ failf(data, "sa_addr inet_ntop() failed with errno %d: %s", errno, Curl_strerror(conn, errno)); diff --git a/lib/connect.h b/lib/connect.h index 397448636..4c038874e 100644 --- a/lib/connect.h +++ b/lib/connect.h @@ -77,6 +77,11 @@ void Curl_updateconninfo(struct connectdata *conn, curl_socket_t sockfd); void Curl_persistconninfo(struct connectdata *conn); int Curl_closesocket(struct connectdata *conn, curl_socket_t sock); +/* + * Get presentation format IP address and port from a sockaddr. + */ +bool Curl_getaddressinfo(struct sockaddr *sa, char *addr, long *port); + /* * The Curl_sockaddr_ex structure is basically libcurl's external API * curl_sockaddr structure with enough space available to directly hold any diff --git a/lib/hostip.c b/lib/hostip.c index 886aeec42..8310c83e1 100644 --- a/lib/hostip.c +++ b/lib/hostip.c @@ -781,7 +781,7 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data) { struct curl_slist *hostp; char hostname[256]; - int port; + int port = 0; for(hostp = data->change.resolve; hostp; hostp = hostp->next) { if(!hostp->data) @@ -819,32 +819,95 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data) } else { struct Curl_dns_entry *dns; - Curl_addrinfo *addr; + Curl_addrinfo *head = NULL, *tail = NULL; char *entry_id; size_t entry_len; - char buffer[256]; - char *address = &buffer[0]; + char address[64]; + char *addresses; + char *addr_begin; + char *addr_end; + char *port_ptr; + char *end_ptr; + char *host_end; + unsigned long tmp_port; + bool error = true; + + host_end = strchr(hostp->data, ':'); + if(!host_end || + ((host_end - hostp->data) >= (ptrdiff_t)sizeof(hostname))) + goto err; + + memcpy(hostname, hostp->data, host_end - hostp->data); + hostname[host_end - hostp->data] = '\0'; + + port_ptr = host_end + 1; + tmp_port = strtoul(port_ptr, &end_ptr, 10); + if(end_ptr == port_ptr || tmp_port > USHRT_MAX || *end_ptr != ':') + goto err; + + port = (int)tmp_port; + addresses = end_ptr + 1; + + while(*end_ptr) { + size_t alen; + Curl_addrinfo *ai; + + addr_begin = end_ptr + 1; + addr_end = strchr(addr_begin, ','); + if(!addr_end) + addr_end = addr_begin + strlen(addr_begin); + end_ptr = addr_end; + + /* allow IP(v6) address within [brackets] */ + if(*addr_begin == '[') { + if(addr_end == addr_begin || *(addr_end - 1) != ']') + goto err; + ++addr_begin; + --addr_end; + } - if(3 != sscanf(hostp->data, "%255[^:]:%d:%255s", hostname, &port, - address)) { - infof(data, "Couldn't parse CURLOPT_RESOLVE entry '%s'!\n", - hostp->data); - continue; - } + alen = addr_end - addr_begin; + if(!alen) + continue; - /* allow IP(v6) address within [brackets] */ - if(address[0] == '[') { - size_t alen = strlen(address); - if(address[alen-1] != ']') - /* it needs to also end with ] to be valid */ + if(alen >= sizeof(address)) + goto err; + + memcpy(address, addr_begin, alen); + address[alen] = '\0'; + +#ifndef ENABLE_IPV6 + if(strchr(address, ':')) { + infof(data, "Ignoring resolve address '%s', missing IPv6 support.\n", + address); continue; - address[alen-1] = 0; /* zero terminate there */ - address++; /* pass the open bracket */ + } +#endif + + ai = Curl_str2addr(address, port); + if(!ai) { + infof(data, "Resolve address '%s' found illegal!\n", address); + goto err; + } + + if(tail) { + tail->ai_next = ai; + tail = tail->ai_next; + } + else { + head = tail = ai; + } } - addr = Curl_str2addr(address, port); - if(!addr) { - infof(data, "Address in '%s' found illegal!\n", hostp->data); + if(!head) + goto err; + + error = false; + err: + if(error) { + infof(data, "Couldn't parse CURLOPT_RESOLVE entry '%s'!\n", + hostp->data); + Curl_freeaddrinfo(head); continue; } @@ -852,10 +915,9 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data) entry_id = create_hostcache_id(hostname, port); /* If we can't create the entry id, fail */ if(!entry_id) { - Curl_freeaddrinfo(addr); + Curl_freeaddrinfo(head); return CURLE_OUT_OF_MEMORY; } - entry_len = strlen(entry_id); if(data->share) @@ -869,7 +931,7 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data) if(!dns) { /* if not in the cache already, put this host in the cache */ - dns = Curl_cache_addr(data, addr, hostname, port); + dns = Curl_cache_addr(data, head, hostname, port); if(dns) { dns->timestamp = 0; /* mark as added by CURLOPT_RESOLVE */ /* release the returned reference; the cache itself will keep the @@ -880,19 +942,19 @@ CURLcode Curl_loadhostpairs(struct Curl_easy *data) else { /* this is a duplicate, free it again */ infof(data, "RESOLVE %s:%d is already cached, %s not stored!\n", - hostname, port, address); - Curl_freeaddrinfo(addr); + hostname, port, addresses); + Curl_freeaddrinfo(head); } if(data->share) Curl_share_unlock(data, CURL_LOCK_DATA_DNS); if(!dns) { - Curl_freeaddrinfo(addr); + Curl_freeaddrinfo(head); return CURLE_OUT_OF_MEMORY; } infof(data, "Added %s:%d:%s to DNS cache\n", - hostname, port, address); + hostname, port, addresses); } } data->change.resolve = NULL; /* dealt with now */ diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index 91e4dd924..892012988 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -172,7 +172,7 @@ test1525 test1526 test1527 test1528 test1529 test1530 test1531 test1532 \ test1533 test1534 test1535 test1536 test1537 test1538 \ test1540 \ test1550 test1551 test1552 test1553 test1554 test1555 test1556 \ -test1600 test1601 test1602 test1603 test1604 test1605 test1606 \ +test1600 test1601 test1602 test1603 test1604 test1605 test1606 test1607 \ \ test1700 test1701 test1702 \ \ diff --git a/tests/data/test1607 b/tests/data/test1607 new file mode 100644 index 000000000..9628324e4 --- /dev/null +++ b/tests/data/test1607 @@ -0,0 +1,26 @@ + + + +unittest +CURLOPT_RESOLVE + + + +# +# Client-side + + +none + + +unittest + + +CURLOPT_RESOLVE parsing + + +unit1607 + + + + diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc index bfb5c4d45..9a19f51d1 100644 --- a/tests/unit/Makefile.inc +++ b/tests/unit/Makefile.inc @@ -9,7 +9,7 @@ UNITPROGS = unit1300 unit1301 unit1302 unit1303 unit1304 unit1305 unit1307 \ unit1308 unit1309 unit1323 \ unit1330 unit1394 unit1395 unit1396 unit1397 unit1398 \ unit1399 \ - unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606 + unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606 unit1607 unit1300_SOURCES = unit1300.c $(UNITFILES) unit1300_CPPFLAGS = $(AM_CPPFLAGS) @@ -85,3 +85,6 @@ unit1605_CPPFLAGS = $(AM_CPPFLAGS) unit1606_SOURCES = unit1606.c $(UNITFILES) unit1606_CPPFLAGS = $(AM_CPPFLAGS) + +unit1607_SOURCES = unit1607.c $(UNITFILES) +unit1607_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/tests/unit/unit1607.c b/tests/unit/unit1607.c new file mode 100644 index 000000000..3e53d4d20 --- /dev/null +++ b/tests/unit/unit1607.c @@ -0,0 +1,203 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 1998 - 2018, Daniel Stenberg, , 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.haxx.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. + * + ***************************************************************************/ +#include "curlcheck.h" + +#include "urldata.h" +#include "connect.h" +#include "share.h" + +#include "memdebug.h" /* LAST include file */ + +static struct Curl_easy *easy; +struct curl_hash *hostcache; + +static CURLcode unit_setup(void) +{ + int res = CURLE_OK; + + global_init(CURL_GLOBAL_ALL); + + easy = curl_easy_init(); + if(!easy) + return CURLE_OUT_OF_MEMORY; + + hostcache = Curl_global_host_cache_init(); + if(!hostcache) + return CURLE_OUT_OF_MEMORY; + + return res; +} + +static void unit_stop(void) +{ + curl_easy_cleanup(easy); + curl_global_cleanup(); +} + +struct testcase { + /* host:port:address[,address]... */ + const char *optval; + + /* lowercase host and port to retrieve the addresses from hostcache */ + const char *host; + int port; + + /* 0 to 9 addresses expected from hostcache */ + const char *address[10]; +}; + + +/* In builds without IPv6 support CURLOPT_RESOLVE should skip over those + addresses, so we have to do that as well. */ +static const char skip = 0; +#ifdef ENABLE_IPV6 +#define IPV6ONLY(x) x +#else +#define IPV6ONLY(x) &skip +#endif + +/* CURLOPT_RESOLVE address parsing tests */ +static const struct testcase tests[] = { + /* spaces aren't allowed, for now */ + { "test.com:80:127.0.0.1, 127.0.0.2", + "test.com", 80, { NULL, } + }, + { "TEST.com:80:,,127.0.0.1,,,127.0.0.2,,,,::1,,,", + "test.com", 80, { "127.0.0.1", "127.0.0.2", IPV6ONLY("::1"), } + }, + { "test.com:80:::1,127.0.0.1", + "test.com", 80, { IPV6ONLY("::1"), "127.0.0.1", } + }, + { "test.com:80:[::1],127.0.0.1", + "test.com", 80, { IPV6ONLY("::1"), "127.0.0.1", } + }, + { "test.com:80:::1", + "test.com", 80, { IPV6ONLY("::1"), } + }, + { "test.com:80:[::1]", + "test.com", 80, { IPV6ONLY("::1"), } + }, + { "test.com:80:127.0.0.1", + "test.com", 80, { "127.0.0.1", } + }, + { "test.com:80:,127.0.0.1", + "test.com", 80, { "127.0.0.1", } + }, + { "test.com:80:127.0.0.1,", + "test.com", 80, { "127.0.0.1", } + }, + { "test.com:0:127.0.0.1", + "test.com", 0, { "127.0.0.1", } + }, +}; + +UNITTEST_START + int i; + int testnum = sizeof(tests) / sizeof(struct testcase); + + for(i = 0; i < testnum; ++i, curl_easy_reset(easy)) { + int j; + int addressnum = sizeof tests[i].address / sizeof *tests[i].address; + struct Curl_addrinfo *addr; + struct Curl_dns_entry *dns; + struct curl_slist *list; + void *entry_id; + bool problem = false; + + Curl_hostcache_clean(easy, hostcache); + easy->dns.hostcache = hostcache; + easy->dns.hostcachetype = HCACHE_GLOBAL; + + list = curl_slist_append(NULL, tests[i].optval); + curl_easy_setopt(easy, CURLOPT_RESOLVE, list); + + Curl_loadhostpairs(easy); + + entry_id = (void *)aprintf("%s:%d", tests[i].host, tests[i].port); + dns = Curl_hash_pick(easy->dns.hostcache, entry_id, strlen(entry_id) + 1); + free(entry_id); + entry_id = NULL; + + addr = dns ? dns->addr : NULL; + + for(j = 0; j < addressnum; ++j) { + long port = 0; + char ipaddress[MAX_IPADR_LEN] = {0}; + + if(!addr && !tests[i].address[j]) + break; + + if(tests[i].address[j] == &skip) + continue; + + if(addr && !Curl_getaddressinfo(addr->ai_addr, + ipaddress, &port)) { + fprintf(stderr, "%s:%d tests[%d] failed. getaddressinfo failed.\n", + __FILE__, __LINE__, i); + problem = true; + break; + } + + if(addr && !tests[i].address[j]) { + fprintf(stderr, "%s:%d tests[%d] failed. the retrieved addr " + "is %s but tests[%d].address[%d] is NULL.\n", + __FILE__, __LINE__, i, ipaddress, i, j); + problem = true; + break; + } + + if(!addr && tests[i].address[j]) { + fprintf(stderr, "%s:%d tests[%d] failed. the retrieved addr " + "is NULL but tests[%d].address[%d] is %s.\n", + __FILE__, __LINE__, i, i, j, tests[i].address[j]); + problem = true; + break; + } + + if(!curl_strequal(ipaddress, tests[i].address[j])) { + fprintf(stderr, "%s:%d tests[%d] failed. the retrieved addr " + "%s is not equal to tests[%d].address[%d] %s.\n", + __FILE__, __LINE__, i, ipaddress, i, j, tests[i].address[j]); + problem = true; + break; + } + + if(port != tests[i].port) { + fprintf(stderr, "%s:%d tests[%d] failed. the retrieved port " + "for tests[%d].address[%d] is %ld but tests[%d].port is %d.\n", + __FILE__, __LINE__, i, i, j, port, i, tests[i].port); + problem = true; + break; + } + + addr = addr->ai_next; + } + + Curl_hostcache_clean(easy, easy->dns.hostcache); + curl_slist_free_all(list); + + if(problem) { + unitfail++; + continue; + } + } +UNITTEST_STOP -- 2.40.0