URI parser/writer for ares_set_servers_csv()/ares_get_servers_csv() (#882)

The DNS server format is insufficient for future configurations, such as
supporting DNS over TLS (DoT) and DNS over HTTPS (DoH), as well as
additional functionality such as domain-specific servers. Already, in
the case where different UDP and TCP ports are used, it is impossible to
represent in the current format.

In order to try to use some standardized format, we are going to define
our own URI schemes that should be parse-able by any URI parser. The new
scheme will only be used when the configuration cannot otherwise be
expressed using the current `ipaddr%iface:port` format, which is the
format used as the nameserver configuration in `/etc/resolv.conf`.
However, the parser `ares_set_servers_csv()` shall accept the new URI
scheme format even when it is not necessary.

This PR implements a URI parser and writer and hooks the basic usage
into `ares_set_servers_csv()` and `ares_get_servers_csv()` as well as
provides updated documentation in the relevant manpages.

We will define these URI schemes:
* `dns://` - Normal DNS server (UDP + TCP). We need to be careful not to
conflict with query params defined in
https://datatracker.ietf.org/doc/html/rfc4501 since we'd technically be
extending this URI scheme. Port defaults to `53`.
* `dns+tls://` - DNS over TLS. Port defaults to `853`.
* `dns+https://` - DNS over HTTPS. Port defaults to `443`.

We initially will define these query parameters (additional arguments
may be required in the future to specify options such as TLS certificate
validation rules):
* `tcpport` - TCP port to use, only for `dns://` scheme. The `port`
specified as part of the `authority` component of the URI will be used
for both UDP and TCP by default, this option will override the TCP port.
* `ipaddr` - Only for `dns+tls://` and `dns+https://`. If the
`authority` component of the URI contains a hostname, this is used to
specify the ip address of the hostname. If not specified, will need to
use a non-secure server to perform a DNS lookup to retrieve this
information. It is always recommended to have both the ip address and
fully qualified domain name specified.
* `hostname` - Only for `dns+tls://` and `dns+https://`. If the
`authority` component of the URI contains an ip address, this is used to
specify the fully qualified domain name of the server. If not specified,
will need to use a non-secure server to perform a DNS reverse lookup to
retrieve this information. It is always recommended to have both the ip
address and fully qualified domain name specified.
* `domain` - If specified, this server is a domain-specific server. Any
queries for this domain will be routed to this server. Multiple servers
may be tagged with the same domain.

Examples:
```
dns://8.8.8.8
dns://[2001:4860:4860::8888]
dns://[fe80::b542:84df:1719:65e3%en0]
dns://192.168.1.1:55
dns://192.168.1.1?tcpport=1153
dns://10.0.1.1?domain=myvpn.com
dns+tls://8.8.8.8?hostname=dns.google
dns+tls://one.one.one.one?ipaddr=1.1.1.1
```

NOTE: While we are defining the scheme for things like domain-specific
servers, DNS over TLS and DNS over HTTPS, the underlying implementations
for those features do not yet exist and therefore will result in errors
if they are attempted to be used.

### Non-compliance in implementation

All these could be easily implemented/fixed if desired, however any such
changes would be of no use to the current c-ares usage of URIs:
* Does not currently support relative references
* Requires use of the authority section, blank is not allowed
* The query string is interpreted to be in
[application/x-www-form-urlencoded](https://en.wikipedia.org/wiki/Application/x-www-form-urlencoded)
format only and will result in parse errors if it is not. This is the
most common format used, however technically not valid to mandate this
format is used. We could add flags in the future to treat the query
string as opaque and leave it to the user to process. Or we could
internally have a list of schemes that use this format.
* [IDNA](https://en.wikipedia.org/wiki/Internationalized_domain_name) is
not supported.
* Does not support hex-encoded IPv4 addresses (this is compliant with RFC3986, but not WHATWG)

Authored-By: Brad House (@bradh352)
pull/886/head
Brad House 2 months ago committed by GitHub
parent b3b5d0b81d
commit 9dd78e2f23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CMakeLists.txt
  2. 2
      README.md
  3. 1
      configure.ac
  4. 88
      docs/ares_set_servers_csv.3
  5. 6
      src/lib/Makefile.inc
  6. 3
      src/lib/ares_config.h.cmake
  7. 2
      src/lib/ares_private.h
  8. 14
      src/lib/ares_search.c
  9. 128
      src/lib/ares_sysconfig_files.c
  10. 140
      src/lib/ares_update_servers.c
  11. 228
      src/lib/dsa/ares_htable_dict.c
  12. 96
      src/lib/include/ares_buf.h
  13. 123
      src/lib/include/ares_htable_dict.h
  14. 8
      src/lib/include/ares_str.h
  15. 216
      src/lib/str/ares_buf.c
  16. 74
      src/lib/str/ares_str.c
  17. 40
      src/lib/str/ares_strsplit.c
  18. 1626
      src/lib/util/ares_uri.c
  19. 252
      src/lib/util/ares_uri.h
  20. 29
      test/ares-test-fuzz-name.c
  21. 2
      test/ares-test-fuzz.c
  22. 77
      test/ares-test-internal.cc
  23. 1
      test/fuzznames/uri1
  24. 1
      test/fuzznames/uri10
  25. 1
      test/fuzznames/uri11
  26. 1
      test/fuzznames/uri12
  27. 1
      test/fuzznames/uri13
  28. 1
      test/fuzznames/uri14
  29. 1
      test/fuzznames/uri16
  30. 1
      test/fuzznames/uri17
  31. 1
      test/fuzznames/uri18
  32. 1
      test/fuzznames/uri19
  33. 1
      test/fuzznames/uri2
  34. 1
      test/fuzznames/uri20
  35. 1
      test/fuzznames/uri21
  36. 1
      test/fuzznames/uri22
  37. 1
      test/fuzznames/uri23
  38. 1
      test/fuzznames/uri24
  39. 1
      test/fuzznames/uri25
  40. 1
      test/fuzznames/uri26
  41. 1
      test/fuzznames/uri27
  42. 1
      test/fuzznames/uri28
  43. 1
      test/fuzznames/uri29
  44. 1
      test/fuzznames/uri3
  45. 1
      test/fuzznames/uri30
  46. 1
      test/fuzznames/uri31
  47. 1
      test/fuzznames/uri32
  48. 1
      test/fuzznames/uri33
  49. 1
      test/fuzznames/uri34
  50. 1
      test/fuzznames/uri35
  51. 1
      test/fuzznames/uri36
  52. 1
      test/fuzznames/uri37
  53. 1
      test/fuzznames/uri4
  54. 1
      test/fuzznames/uri5
  55. 1
      test/fuzznames/uri6
  56. 1
      test/fuzznames/uri7
  57. 1
      test/fuzznames/uri8
  58. 1
      test/fuzznames/uri9

@ -406,7 +406,7 @@ ENDIF ()
CHECK_STRUCT_HAS_MEMBER("struct sockaddr_in6" sin6_scope_id "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID LANGUAGE C) CHECK_STRUCT_HAS_MEMBER("struct sockaddr_in6" sin6_scope_id "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID LANGUAGE C)
CHECK_SYMBOL_EXISTS (memmem "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_MEMMEM)
CHECK_SYMBOL_EXISTS (closesocket "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_CLOSESOCKET) CHECK_SYMBOL_EXISTS (closesocket "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_CLOSESOCKET)
CHECK_SYMBOL_EXISTS (CloseSocket "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_CLOSESOCKET_CAMEL) CHECK_SYMBOL_EXISTS (CloseSocket "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_CLOSESOCKET_CAMEL)
CHECK_SYMBOL_EXISTS (connect "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_CONNECT) CHECK_SYMBOL_EXISTS (connect "${CMAKE_EXTRA_INCLUDE_FILES}" HAVE_CONNECT)

@ -154,3 +154,5 @@ See [Features](FEATURES.md)
IPv6 address sorting as used by `ares_getaddrinfo()`. IPv6 address sorting as used by `ares_getaddrinfo()`.
- [RFC7413](https://datatracker.ietf.org/doc/html/rfc7413). - [RFC7413](https://datatracker.ietf.org/doc/html/rfc7413).
TCP FastOpen (TFO) for 0-RTT TCP Connection Resumption. TCP FastOpen (TFO) for 0-RTT TCP Connection Resumption.
- [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986).
Uniform Resource Identifier (URI). Used for server configuration.

@ -540,6 +540,7 @@ dnl https://mailman.videolan.org/pipermail/vlc-devel/2015-March/101802.html
dnl which would require we check each individually and provide function arguments dnl which would require we check each individually and provide function arguments
dnl for the test. dnl for the test.
AC_CHECK_DECL(memmem, [AC_DEFINE([HAVE_MEMMEM], 1, [Define to 1 if you have `memmem`] )], [], $cares_all_includes)
AC_CHECK_DECL(recv, [AC_DEFINE([HAVE_RECV], 1, [Define to 1 if you have `recv`] )], [], $cares_all_includes) AC_CHECK_DECL(recv, [AC_DEFINE([HAVE_RECV], 1, [Define to 1 if you have `recv`] )], [], $cares_all_includes)
AC_CHECK_DECL(recvfrom, [AC_DEFINE([HAVE_RECVFROM], 1, [Define to 1 if you have `recvfrom`] )], [], $cares_all_includes) AC_CHECK_DECL(recvfrom, [AC_DEFINE([HAVE_RECVFROM], 1, [Define to 1 if you have `recvfrom`] )], [], $cares_all_includes)
AC_CHECK_DECL(send, [AC_DEFINE([HAVE_SEND], 1, [Define to 1 if you have `send`] )], [], $cares_all_includes) AC_CHECK_DECL(send, [AC_DEFINE([HAVE_SEND], 1, [Define to 1 if you have `send`] )], [], $cares_all_includes)

@ -29,11 +29,18 @@ simulation but unlikely to be useful in production.
The \fBares_get_servers_csv\fP retrieves the list of servers in comma delimited The \fBares_get_servers_csv\fP retrieves the list of servers in comma delimited
format. format.
The input and output format is a comma separated list of servers. Each server The input and output format is a comma separated list of servers. Two formats
entry may contain these forms: are available, the typical \fBresolv.conf(5)\fP \fInameserver\fP format, as
well as a \fIURI\fP format. Both formats can be used at the same time in the
provided CSV string.
The \fInameserver\fP format is:
.nf
ip[:port][%iface] ip[:port][%iface]
.fi
.RS 4
The \fBip\fP may be encapsulated in square brackets ([ ]), and must be if The \fBip\fP may be encapsulated in square brackets ([ ]), and must be if
using ipv6 and also specifying a port. using ipv6 and also specifying a port.
@ -42,16 +49,88 @@ The \fBport\fP is optional, and will default to 53 or the value specified in
The \fBiface\fP is specific to IPv6 link-local servers (fe80::/10) and should The \fBiface\fP is specific to IPv6 link-local servers (fe80::/10) and should
not otherwise be used. not otherwise be used.
.RE
\fInameserver\fP format examples:
.nf
192.168.1.100
192.168.1.101:53
[1:2:3::4]:53
[fe80::1]:53%eth0
.fi
.PP
The \fIURI\fP format is is made up of these defined schemes:
.RS 4
\fIdns://\fP - Normal DNS server (UDP + TCP). We need to be careful not to
conflict with query params defined in RFC4501 since we'd technically be
extending this URI scheme. Port defaults to 53.
\fIdns+tls://\fP - DNS over TLS. Port defaults to 853.
\fIdns+https://\fP - DNS over HTTPS. Port defaults to 443.
.RE
.PP
Query parameters are defined as below. Additional parameters may be defined
in the future.
.RS 4
\fItcpport\fP - TCP port to use, only for \fIdns://\fP scheme. The port
specified as part of the authority component of the URI will be used for both
UDP and TCP by default, this option will override the TCP port.
\fIipaddr\fP - Only for \fIdns+tls://\fP and \fIdns+https://\fP. If the
authority component of the URI contains a hostname, this is used to specify the
ip address of the hostname. If not specified, will need to use a non-secure
server to perform a DNS lookup to retrieve this information. It is always
recommended to have both the ip address and fully qualified domain name
specified.
\fIhostname\fP - Only for \fIdns+tls://\fP and \fIdns+https://\fP. If the
authority component of the URI contains an ip address, this is used to specify
the fully qualified domain name of the server. If not specified, will need to
use a non-secure server to perform a DNS reverse lookup to retrieve this
information. It is always recommended to have both the ip address and fully
qualified domain name specified.
\fIdomain\fP - If specified, this server is a domain-specific server. Any
queries for this domain will be routed to this server. Multiple servers may be
tagged with the same domain.
.RE
\fIURI\fP format Examples:
.nf
For example: dns://8.8.8.8
dns://[2001:4860:4860::8888]
dns://[fe80::b542:84df:1719:65e3%en0]
dns://192.168.1.1:55
dns://192.168.1.1?tcpport=1153
dns://10.0.1.1?domain=myvpn.com
dns+tls://8.8.8.8?hostname=dns.google
dns+tls://one.one.one.one?ipaddr=1.1.1.1
.fi
\fBNOTE\fP: While we are defining the scheme for things like domain-specific
servers, DNS over TLS and DNS over HTTPS, the underlying implementations for
those features do not yet exist and therefore will result in errors if they are
attempted to be used.
192.168.1.100,192.168.1.101:53,[1:2:3::4]:53,[fe80::1]:53%eth0
.PP .PP
As of c-ares 1.24.0, \fBares_set_servers_csv\fP and \fBares_set_servers_ports_csv\fP As of c-ares 1.24.0, \fBares_set_servers_csv\fP and \fBares_set_servers_ports_csv\fP
are identical. Prior versions would simply omit ports in \fBares_set_servers_csv\fP are identical. Prior versions would simply omit ports in \fBares_set_servers_csv\fP
but due to the addition of link local interface support, this difference was but due to the addition of link local interface support, this difference was
removed. removed.
.SH EXAMPLE
.nf
192.168.1.100,[fe80::1]:53%eth0,dns://192.168.1.1?tcpport=1153
.fi
.SH RETURN VALUES .SH RETURN VALUES
.B ares_set_servers_csv(3) .B ares_set_servers_csv(3)
and and
@ -81,3 +160,4 @@ returns a string representing the servers configured which must be freed with
\fBares_set_servers_csv\fP was added in c-ares 1.7.2 \fBares_set_servers_csv\fP was added in c-ares 1.7.2
\fBares_set_servers_ports_csv\fP was added in c-ares 1.11.0. \fBares_set_servers_ports_csv\fP was added in c-ares 1.11.0.
\fBares_get_servers_csv\fP was added in c-ares 1.24.0. \fBares_get_servers_csv\fP was added in c-ares 1.24.0.
\fIURI\fP support was added in c-ares 1.34.0.

@ -45,6 +45,7 @@ CSOURCES = ares_addrinfo2hostent.c \
dsa/ares_array.c \ dsa/ares_array.c \
dsa/ares_htable.c \ dsa/ares_htable.c \
dsa/ares_htable_asvp.c \ dsa/ares_htable_asvp.c \
dsa/ares_htable_dict.c \
dsa/ares_htable_strvp.c \ dsa/ares_htable_strvp.c \
dsa/ares_htable_szvp.c \ dsa/ares_htable_szvp.c \
dsa/ares_htable_vpstr.c \ dsa/ares_htable_vpstr.c \
@ -88,7 +89,8 @@ CSOURCES = ares_addrinfo2hostent.c \
util/ares_threads.c \ util/ares_threads.c \
util/ares_timeval.c \ util/ares_timeval.c \
util/ares_math.c \ util/ares_math.c \
util/ares_rand.c util/ares_rand.c \
util/ares_uri.c
HHEADERS = ares_android.h \ HHEADERS = ares_android.h \
ares_conn.h \ ares_conn.h \
@ -106,6 +108,7 @@ HHEADERS = ares_android.h \
include/ares_array.h \ include/ares_array.h \
include/ares_buf.h \ include/ares_buf.h \
include/ares_htable_asvp.h \ include/ares_htable_asvp.h \
include/ares_htable_dict.h \
include/ares_htable_strvp.h \ include/ares_htable_strvp.h \
include/ares_htable_szvp.h \ include/ares_htable_szvp.h \
include/ares_htable_vpstr.h \ include/ares_htable_vpstr.h \
@ -120,4 +123,5 @@ HHEADERS = ares_android.h \
util/ares_math.h \ util/ares_math.h \
util/ares_rand.h \ util/ares_rand.h \
util/ares_time.h \ util/ares_time.h \
util/ares_uri.h \
thirdparty/apple/dnsinfo.h thirdparty/apple/dnsinfo.h

@ -82,6 +82,9 @@
/* Define to 1 if you have the <poll.h> header file. */ /* Define to 1 if you have the <poll.h> header file. */
#cmakedefine HAVE_POLL_H 1 #cmakedefine HAVE_POLL_H 1
/* Define to 1 if you have the memmem function. */
#cmakedefine HAVE_MEMMEM 1
/* Define to 1 if you have the poll function. */ /* Define to 1 if you have the poll function. */
#cmakedefine HAVE_POLL 1 #cmakedefine HAVE_POLL 1

@ -51,6 +51,7 @@
#include "ares_htable_strvp.h" #include "ares_htable_strvp.h"
#include "ares_htable_szvp.h" #include "ares_htable_szvp.h"
#include "ares_htable_asvp.h" #include "ares_htable_asvp.h"
#include "ares_htable_dict.h"
#include "ares_htable_vpvp.h" #include "ares_htable_vpvp.h"
#include "ares_htable_vpstr.h" #include "ares_htable_vpstr.h"
#include "record/ares_dns_multistring.h" #include "record/ares_dns_multistring.h"
@ -62,6 +63,7 @@
#include "ares_conn.h" #include "ares_conn.h"
#include "ares_str.h" #include "ares_str.h"
#include "str/ares_strsplit.h" #include "str/ares_strsplit.h"
#include "util/ares_uri.h"
#ifndef HAVE_GETENV #ifndef HAVE_GETENV
# include "ares_getenv.h" # include "ares_getenv.h"

@ -515,8 +515,9 @@ ares_status_t ares_lookup_hostaliases(const ares_channel_t *channel,
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
const char *hostaliases = NULL; const char *hostaliases = NULL;
ares_buf_t *buf = NULL; ares_buf_t *buf = NULL;
ares_llist_t *lines = NULL; ares_array_t *lines = NULL;
ares_llist_node_t *node; size_t num;
size_t i;
if (channel == NULL || name == NULL || alias == NULL) { if (channel == NULL || name == NULL || alias == NULL) {
return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
@ -565,9 +566,10 @@ ares_status_t ares_lookup_hostaliases(const ares_channel_t *channel,
goto done; goto done;
} }
for (node = ares_llist_node_first(lines); node != NULL; num = ares_array_len(lines);
node = ares_llist_node_next(node)) { for (i = 0; i < num; i++) {
ares_buf_t *line = ares_llist_node_val(node); ares_buf_t **bufptr = ares_array_at(lines, i);
ares_buf_t *line = *bufptr;
char hostname[64] = ""; char hostname[64] = "";
char fqdn[256] = ""; char fqdn[256] = "";
@ -615,7 +617,7 @@ ares_status_t ares_lookup_hostaliases(const ares_channel_t *channel,
done: done:
ares_buf_destroy(buf); ares_buf_destroy(buf);
ares_llist_destroy(lines); ares_array_destroy(lines);
return status; return status;
} }

@ -213,9 +213,10 @@ ares_status_t ares_parse_sortlist(struct apattern **sortlist, size_t *nsort,
const char *str) const char *str)
{ {
ares_buf_t *buf = NULL; ares_buf_t *buf = NULL;
ares_llist_t *list = NULL;
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
ares_llist_node_t *node = NULL; ares_array_t *arr = NULL;
size_t num = 0;
size_t i;
if (sortlist == NULL || nsort == NULL || str == NULL) { if (sortlist == NULL || nsort == NULL || str == NULL) {
return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
@ -236,14 +237,15 @@ ares_status_t ares_parse_sortlist(struct apattern **sortlist, size_t *nsort,
/* Split on space or semicolon */ /* Split on space or semicolon */
status = ares_buf_split(buf, (const unsigned char *)" ;", 2, status = ares_buf_split(buf, (const unsigned char *)" ;", 2,
ARES_BUF_SPLIT_NONE, 0, &list); ARES_BUF_SPLIT_NONE, 0, &arr);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
} }
for (node = ares_llist_node_first(list); node != NULL; num = ares_array_len(arr);
node = ares_llist_node_next(node)) { for (i = 0; i < num; i++) {
ares_buf_t *entry = ares_llist_node_val(node); ares_buf_t **bufptr = ares_array_at(arr, i);
ares_buf_t *entry = *bufptr;
struct apattern pat; struct apattern pat;
@ -266,7 +268,7 @@ ares_status_t ares_parse_sortlist(struct apattern **sortlist, size_t *nsort,
done: done:
ares_buf_destroy(buf); ares_buf_destroy(buf);
ares_llist_destroy(list); ares_array_destroy(arr);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
ares_free(*sortlist); ares_free(*sortlist);
@ -322,28 +324,21 @@ static ares_status_t config_lookup(ares_sysconfig_t *sysconfig, ares_buf_t *buf,
ares_status_t status; ares_status_t status;
char lookupstr[32]; char lookupstr[32];
size_t lookupstr_cnt = 0; size_t lookupstr_cnt = 0;
ares_llist_t *lookups = NULL; char **lookups = NULL;
ares_llist_node_t *node; size_t num = 0;
size_t i;
size_t separators_len = ares_strlen(separators); size_t separators_len = ares_strlen(separators);
status = ares_buf_split(buf, (const unsigned char *)separators, status =
separators_len, ARES_BUF_SPLIT_TRIM, 0, &lookups); ares_buf_split_str(buf, (const unsigned char *)separators, separators_len,
ARES_BUF_SPLIT_TRIM, 0, &lookups, &num);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
} }
memset(lookupstr, 0, sizeof(lookupstr)); for (i = 0; i < num; i++) {
const char *value = lookups[i];
for (node = ares_llist_node_first(lookups); node != NULL;
node = ares_llist_node_next(node)) {
char value[128];
char ch; char ch;
ares_buf_t *valbuf = ares_llist_node_val(node);
status = buf_fetch_string(valbuf, value, sizeof(value));
if (status != ARES_SUCCESS) {
continue;
}
if (ares_strcaseeq(value, "dns") || ares_strcaseeq(value, "bind") || if (ares_strcaseeq(value, "dns") || ares_strcaseeq(value, "bind") ||
ares_strcaseeq(value, "resolv") || ares_strcaseeq(value, "resolve")) { ares_strcaseeq(value, "resolv") || ares_strcaseeq(value, "resolve")) {
@ -363,10 +358,12 @@ static ares_status_t config_lookup(ares_sysconfig_t *sysconfig, ares_buf_t *buf,
} }
if (lookupstr_cnt) { if (lookupstr_cnt) {
lookupstr[lookupstr_cnt] = 0;
ares_free(sysconfig->lookups); ares_free(sysconfig->lookups);
sysconfig->lookups = ares_strdup(lookupstr); sysconfig->lookups = ares_strdup(lookupstr);
if (sysconfig->lookups == NULL) { if (sysconfig->lookups == NULL) {
return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
goto done; /* LCOV_EXCL_LINE: OutOfMemory */
} }
} }
@ -376,35 +373,35 @@ done:
if (status != ARES_ENOMEM) { if (status != ARES_ENOMEM) {
status = ARES_SUCCESS; status = ARES_SUCCESS;
} }
ares_llist_destroy(lookups); ares_free_array(lookups, num, ares_free);
return status; return status;
} }
static ares_status_t process_option(ares_sysconfig_t *sysconfig, static ares_status_t process_option(ares_sysconfig_t *sysconfig,
ares_buf_t *option) ares_buf_t *option)
{ {
ares_llist_t *kv = NULL; char **kv = NULL;
char key[32] = ""; size_t num = 0;
char val[32] = ""; const char *key;
const char *val;
unsigned int valint = 0; unsigned int valint = 0;
ares_status_t status; ares_status_t status;
/* Split on : */ /* Split on : */
status = ares_buf_split(option, (const unsigned char *)":", 1, status = ares_buf_split_str(option, (const unsigned char *)":", 1,
ARES_BUF_SPLIT_TRIM, 2, &kv); ARES_BUF_SPLIT_TRIM, 2, &kv, &num);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
} }
status = buf_fetch_string(ares_llist_first_val(kv), key, sizeof(key)); if (num < 1) {
if (status != ARES_SUCCESS) { status = ARES_EBADSTR;
goto done;
}
if (ares_llist_len(kv) == 2) {
status = buf_fetch_string(ares_llist_last_val(kv), val, sizeof(val));
if (status != ARES_SUCCESS) {
goto done; goto done;
} }
key = kv[0];
if (num == 2) {
val = kv[1];
valint = (unsigned int)strtoul(val, NULL, 10); valint = (unsigned int)strtoul(val, NULL, 10);
} }
@ -427,7 +424,7 @@ static ares_status_t process_option(ares_sysconfig_t *sysconfig,
} }
done: done:
ares_llist_destroy(kv); ares_free_array(kv, num, ares_free);
return status; return status;
} }
@ -435,9 +432,10 @@ ares_status_t ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
const char *str) const char *str)
{ {
ares_buf_t *buf = NULL; ares_buf_t *buf = NULL;
ares_llist_t *options = NULL; ares_array_t *options = NULL;
size_t num;
size_t i;
ares_status_t status; ares_status_t status;
ares_llist_node_t *node;
buf = ares_buf_create_const((const unsigned char *)str, ares_strlen(str)); buf = ares_buf_create_const((const unsigned char *)str, ares_strlen(str));
if (buf == NULL) { if (buf == NULL) {
@ -450,9 +448,10 @@ ares_status_t ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
goto done; goto done;
} }
for (node = ares_llist_node_first(options); node != NULL; num = ares_array_len(options);
node = ares_llist_node_next(node)) { for (i = 0; i < num; i++) {
ares_buf_t *valbuf = ares_llist_node_val(node); ares_buf_t **bufptr = ares_array_at(options, i);
ares_buf_t *valbuf = *bufptr;
status = process_option(sysconfig, valbuf); status = process_option(sysconfig, valbuf);
/* Out of memory is the only fatal condition */ /* Out of memory is the only fatal condition */
@ -464,7 +463,7 @@ ares_status_t ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
status = ARES_SUCCESS; status = ARES_SUCCESS;
done: done:
ares_llist_destroy(options); ares_array_destroy(options);
ares_buf_destroy(buf); ares_buf_destroy(buf);
return status; return status;
} }
@ -625,9 +624,10 @@ static ares_status_t parse_nsswitch_line(ares_sysconfig_t *sysconfig,
ares_buf_t *line) ares_buf_t *line)
{ {
char option[32]; char option[32];
ares_buf_t *buf;
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
ares_llist_t *sects = NULL; ares_array_t *sects = NULL;
ares_buf_t **bufptr;
ares_buf_t *buf;
/* Ignore lines beginning with a comment */ /* Ignore lines beginning with a comment */
if (ares_buf_begins_with(line, (const unsigned char *)"#", 1)) { if (ares_buf_begins_with(line, (const unsigned char *)"#", 1)) {
@ -638,11 +638,13 @@ static ares_status_t parse_nsswitch_line(ares_sysconfig_t *sysconfig,
status = ares_buf_split(line, (const unsigned char *)":", 1, status = ares_buf_split(line, (const unsigned char *)":", 1,
ARES_BUF_SPLIT_TRIM, 2, &sects); ARES_BUF_SPLIT_TRIM, 2, &sects);
if (status != ARES_SUCCESS || ares_llist_len(sects) != 2) { if (status != ARES_SUCCESS || ares_array_len(sects) != 2) {
goto done; goto done;
} }
buf = ares_llist_first_val(sects); bufptr = ares_array_at(sects, 0);
buf = *bufptr;
status = buf_fetch_string(buf, option, sizeof(option)); status = buf_fetch_string(buf, option, sizeof(option));
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
@ -654,11 +656,12 @@ static ares_status_t parse_nsswitch_line(ares_sysconfig_t *sysconfig,
} }
/* Values are space separated */ /* Values are space separated */
buf = ares_llist_last_val(sects); bufptr = ares_array_at(sects, 1);
buf = *bufptr;
status = config_lookup(sysconfig, buf, " \t"); status = config_lookup(sysconfig, buf, " \t");
done: done:
ares_llist_destroy(sects); ares_array_destroy(sects);
if (status != ARES_ENOMEM) { if (status != ARES_ENOMEM) {
status = ARES_SUCCESS; status = ARES_SUCCESS;
} }
@ -672,9 +675,10 @@ static ares_status_t parse_svcconf_line(ares_sysconfig_t *sysconfig,
ares_buf_t *line) ares_buf_t *line)
{ {
char option[32]; char option[32];
ares_buf_t **bufptr;
ares_buf_t *buf; ares_buf_t *buf;
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
ares_llist_t *sects = NULL; ares_array_t *sects = NULL;
/* Ignore lines beginning with a comment */ /* Ignore lines beginning with a comment */
if (ares_buf_begins_with(line, (const unsigned char *)"#", 1)) { if (ares_buf_begins_with(line, (const unsigned char *)"#", 1)) {
@ -685,11 +689,12 @@ static ares_status_t parse_svcconf_line(ares_sysconfig_t *sysconfig,
status = ares_buf_split(line, (const unsigned char *)"=", 1, status = ares_buf_split(line, (const unsigned char *)"=", 1,
ARES_BUF_SPLIT_TRIM, 2, &sects); ARES_BUF_SPLIT_TRIM, 2, &sects);
if (status != ARES_SUCCESS || ares_llist_len(sects) != 2) { if (status != ARES_SUCCESS || ares_array_len(sects) != 2) {
goto done; goto done;
} }
buf = ares_llist_first_val(sects); bufptr = ares_array_at(sects, 0);
buf = *bufptr;
status = buf_fetch_string(buf, option, sizeof(option)); status = buf_fetch_string(buf, option, sizeof(option));
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
@ -701,11 +706,12 @@ static ares_status_t parse_svcconf_line(ares_sysconfig_t *sysconfig,
} }
/* Values are comma separated */ /* Values are comma separated */
buf = ares_llist_last_val(sects); bufptr = ares_array_at(sects, 1);
buf = *bufptr;
status = config_lookup(sysconfig, buf, ","); status = config_lookup(sysconfig, buf, ",");
done: done:
ares_llist_destroy(sects); ares_array_destroy(sects);
if (status != ARES_ENOMEM) { if (status != ARES_ENOMEM) {
status = ARES_SUCCESS; status = ARES_SUCCESS;
} }
@ -727,9 +733,10 @@ static ares_status_t process_config_lines(const char *filename,
line_callback_t cb) line_callback_t cb)
{ {
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
ares_llist_node_t *node; ares_array_t *lines = NULL;
ares_llist_t *lines = NULL;
ares_buf_t *buf = NULL; ares_buf_t *buf = NULL;
size_t num;
size_t i;
buf = ares_buf_create(); buf = ares_buf_create();
if (buf == NULL) { if (buf == NULL) {
@ -748,9 +755,10 @@ static ares_status_t process_config_lines(const char *filename,
goto done; goto done;
} }
for (node = ares_llist_node_first(lines); node != NULL; num = ares_array_len(lines);
node = ares_llist_node_next(node)) { for (i = 0; i < num; i++) {
ares_buf_t *line = ares_llist_node_val(node); ares_buf_t **bufptr = ares_array_at(lines, i);
ares_buf_t *line = *bufptr;
status = cb(sysconfig, line); status = cb(sysconfig, line);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
@ -760,7 +768,7 @@ static ares_status_t process_config_lines(const char *filename,
done: done:
ares_buf_destroy(buf); ares_buf_destroy(buf);
ares_llist_destroy(lines); ares_array_destroy(lines);
return status; return status;
} }

@ -39,6 +39,9 @@
#ifdef HAVE_NET_IF_H #ifdef HAVE_NET_IF_H
# include <net/if.h> # include <net/if.h>
#endif #endif
#ifdef HAVE_STDINT_H
# include <stdint.h>
#endif
#if defined(USE_WINSOCK) #if defined(USE_WINSOCK)
# if defined(HAVE_IPHLPAPI_H) # if defined(HAVE_IPHLPAPI_H)
@ -192,6 +195,53 @@ static ares_bool_t ares_server_blacklisted(const struct ares_addr *addr)
return ARES_FALSE; return ARES_FALSE;
} }
static ares_status_t parse_nameserver_uri(ares_buf_t *buf,
ares_sconfig_t *sconfig)
{
ares_uri_t *uri = NULL;
ares_status_t status = ARES_SUCCESS;
const char *port;
char *ll_scope;
char hoststr[256];
size_t addrlen;
status = ares_uri_parse_buf(&uri, buf);
if (status != ARES_SUCCESS) {
return status;
}
if (!ares_streq("dns", ares_uri_get_scheme(uri))) {
status = ARES_EBADSTR;
goto done;
}
ares_strcpy(hoststr, ares_uri_get_host(uri), sizeof(hoststr));
ll_scope = strchr(hoststr, '%');
if (ll_scope != NULL) {
*ll_scope = 0;
ll_scope++;
ares_strcpy(sconfig->ll_iface, ll_scope, sizeof(sconfig->ll_iface));
}
/* Convert ip address from string to network byte order */
sconfig->addr.family = AF_UNSPEC;
if (ares_dns_pton(hoststr, &sconfig->addr, &addrlen) == NULL) {
status = ARES_EBADSTR;
goto done;
}
sconfig->udp_port = ares_uri_get_port(uri);
sconfig->tcp_port = sconfig->udp_port;
port = ares_uri_get_query_key(uri, "tcpport");
if (port != NULL) {
sconfig->tcp_port = (unsigned short)atoi(port);
}
done:
ares_uri_destroy(uri);
return status;
}
/* Parse address and port in these formats, either ipv4 or ipv6 addresses /* Parse address and port in these formats, either ipv4 or ipv6 addresses
* are allowed: * are allowed:
* ipaddr * ipaddr
@ -232,7 +282,7 @@ static ares_status_t parse_nameserver(ares_buf_t *buf, ares_sconfig_t *sconfig)
/* Consume until ] */ /* Consume until ] */
if (ares_buf_consume_until_charset(buf, (const unsigned char *)"]", 1, if (ares_buf_consume_until_charset(buf, (const unsigned char *)"]", 1,
ARES_TRUE) == 0) { ARES_TRUE) == SIZE_MAX) {
return ARES_EBADSTR; return ARES_EBADSTR;
} }
@ -453,8 +503,9 @@ ares_status_t ares_sconfig_append_fromstr(ares_llist_t **sconfig,
{ {
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
ares_buf_t *buf = NULL; ares_buf_t *buf = NULL;
ares_llist_t *list = NULL; ares_array_t *list = NULL;
ares_llist_node_t *node; size_t num;
size_t i;
/* On Windows, there may be more than one nameserver specified in the same /* On Windows, there may be more than one nameserver specified in the same
* registry key, so we parse input as a space or comma separated list. * registry key, so we parse input as a space or comma separated list.
@ -471,12 +522,17 @@ ares_status_t ares_sconfig_append_fromstr(ares_llist_t **sconfig,
goto done; goto done;
} }
for (node = ares_llist_node_first(list); node != NULL; num = ares_array_len(list);
node = ares_llist_node_next(node)) { for (i = 0; i < num; i++) {
ares_buf_t *entry = ares_llist_node_val(node); ares_buf_t **bufptr = ares_array_at(list, i);
ares_buf_t *entry = *bufptr;
ares_sconfig_t s; ares_sconfig_t s;
status = parse_nameserver_uri(entry, &s);
if (status != ARES_SUCCESS) {
status = parse_nameserver(entry, &s); status = parse_nameserver(entry, &s);
}
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
if (ignore_invalid) { if (ignore_invalid) {
continue; continue;
@ -495,7 +551,7 @@ ares_status_t ares_sconfig_append_fromstr(ares_llist_t **sconfig,
status = ARES_SUCCESS; status = ARES_SUCCESS;
done: done:
ares_llist_destroy(list); ares_array_destroy(list);
ares_buf_destroy(buf); ares_buf_destroy(buf);
return status; return status;
} }
@ -915,12 +971,82 @@ fail:
/* LCOV_EXCL_STOP */ /* LCOV_EXCL_STOP */
} }
static ares_bool_t ares_server_use_uri(const ares_server_t *server)
{
/* Currently only reason to use new format is if the ports for udp and tcp
* are different */
if (server->tcp_port != server->udp_port) {
return ARES_TRUE;
}
return ARES_FALSE;
}
static ares_status_t ares_get_server_addr_uri(const ares_server_t *server,
ares_buf_t *buf)
{
ares_uri_t *uri = NULL;
ares_status_t status;
char addr[INET6_ADDRSTRLEN];
uri = ares_uri_create();
if (uri == NULL) {
return ARES_ENOMEM;
}
status = ares_uri_set_scheme(uri, "dns");
if (status != ARES_SUCCESS) {
goto done;
}
ares_inet_ntop(server->addr.family, &server->addr.addr, addr, sizeof(addr));
if (ares_strlen(server->ll_iface)) {
char addr_iface[256];
snprintf(addr_iface, sizeof(addr_iface), "%s%%%s", addr, server->ll_iface);
status = ares_uri_set_host(uri, addr_iface);
} else {
status = ares_uri_set_host(uri, addr);
}
if (status != ARES_SUCCESS) {
goto done;
}
status = ares_uri_set_port(uri, server->udp_port);
if (status != ARES_SUCCESS) {
goto done;
}
if (server->udp_port != server->tcp_port) {
char port[6];
snprintf(port, sizeof(port), "%d", server->tcp_port);
status = ares_uri_set_query_key(uri, "tcpport", port);
if (status != ARES_SUCCESS) {
goto done;
}
}
status = ares_uri_write_buf(uri, buf);
if (status != ARES_SUCCESS) {
goto done;
}
done:
ares_uri_destroy(uri);
return status;
}
/* Write out the details of a server to a buffer */ /* Write out the details of a server to a buffer */
ares_status_t ares_get_server_addr(const ares_server_t *server, ares_buf_t *buf) ares_status_t ares_get_server_addr(const ares_server_t *server, ares_buf_t *buf)
{ {
ares_status_t status; ares_status_t status;
char addr[INET6_ADDRSTRLEN]; char addr[INET6_ADDRSTRLEN];
if (ares_server_use_uri(server)) {
return ares_get_server_addr_uri(server, buf);
}
/* ipv4addr or [ipv6addr] */ /* ipv4addr or [ipv6addr] */
if (server->addr.family == AF_INET6) { if (server->addr.family == AF_INET6) {
status = ares_buf_append_byte(buf, '['); status = ares_buf_append_byte(buf, '[');

@ -0,0 +1,228 @@
/* MIT License
*
* Copyright (c) 2024 Brad House
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#include "ares_private.h"
#include "ares_htable.h"
#include "ares_htable_dict.h"
struct ares_htable_dict {
ares_htable_t *hash;
};
typedef struct {
char *key;
char *val;
ares_htable_dict_t *parent;
} ares_htable_dict_bucket_t;
void ares_htable_dict_destroy(ares_htable_dict_t *htable)
{
if (htable == NULL) {
return; /* LCOV_EXCL_LINE: DefensiveCoding */
}
ares_htable_destroy(htable->hash);
ares_free(htable);
}
static unsigned int hash_func(const void *key, unsigned int seed)
{
return ares_htable_hash_FNV1a_casecmp(key, ares_strlen(key), seed);
}
static const void *bucket_key(const void *bucket)
{
const ares_htable_dict_bucket_t *arg = bucket;
return arg->key;
}
static void bucket_free(void *bucket)
{
ares_htable_dict_bucket_t *arg = bucket;
ares_free(arg->key);
ares_free(arg->val);
ares_free(arg);
}
static ares_bool_t key_eq(const void *key1, const void *key2)
{
return ares_strcaseeq(key1, key2);
}
ares_htable_dict_t *ares_htable_dict_create(void)
{
ares_htable_dict_t *htable = ares_malloc(sizeof(*htable));
if (htable == NULL) {
goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
}
htable->hash = ares_htable_create(hash_func, bucket_key, bucket_free, key_eq);
if (htable->hash == NULL) {
goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
}
return htable;
/* LCOV_EXCL_START: OutOfMemory */
fail:
if (htable) {
ares_htable_destroy(htable->hash);
ares_free(htable);
}
return NULL;
/* LCOV_EXCL_STOP */
}
ares_bool_t ares_htable_dict_insert(ares_htable_dict_t *htable, const char *key,
const char *val)
{
ares_htable_dict_bucket_t *bucket = NULL;
if (htable == NULL || ares_strlen(key) == 0) {
goto fail;
}
bucket = ares_malloc_zero(sizeof(*bucket));
if (bucket == NULL) {
goto fail;
}
bucket->parent = htable;
bucket->key = ares_strdup(key);
if (bucket->key == NULL) {
goto fail;
}
if (val != NULL) {
bucket->val = ares_strdup(val);
if (bucket->val == NULL) {
goto fail;
}
}
if (!ares_htable_insert(htable->hash, bucket)) {
goto fail;
}
return ARES_TRUE;
fail:
if (bucket) {
ares_free(bucket->val);
ares_free(bucket);
}
return ARES_FALSE;
}
ares_bool_t ares_htable_dict_get(const ares_htable_dict_t *htable,
const char *key, const char **val)
{
const ares_htable_dict_bucket_t *bucket = NULL;
if (val) {
*val = NULL;
}
if (htable == NULL) {
return ARES_FALSE;
}
bucket = ares_htable_get(htable->hash, key);
if (bucket == NULL) {
return ARES_FALSE;
}
if (val) {
*val = bucket->val;
}
return ARES_TRUE;
}
const char *ares_htable_dict_get_direct(const ares_htable_dict_t *htable,
const char *key)
{
const char *val = NULL;
ares_htable_dict_get(htable, key, &val);
return val;
}
ares_bool_t ares_htable_dict_remove(ares_htable_dict_t *htable, const char *key)
{
if (htable == NULL) {
return ARES_FALSE;
}
return ares_htable_remove(htable->hash, key);
}
size_t ares_htable_dict_num_keys(const ares_htable_dict_t *htable)
{
if (htable == NULL) {
return 0;
}
return ares_htable_num_keys(htable->hash);
}
char **ares_htable_dict_keys(const ares_htable_dict_t *htable, size_t *num)
{
const void **buckets = NULL;
size_t cnt = 0;
char **out = NULL;
size_t i;
if (htable == NULL || num == NULL) {
return NULL; /* LCOV_EXCL_LINE: DefensiveCoding */
}
*num = 0;
buckets = ares_htable_all_buckets(htable->hash, &cnt);
if (buckets == NULL || cnt == 0) {
return NULL;
}
out = ares_malloc_zero(sizeof(*out) * cnt);
if (out == NULL) {
goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
}
for (i = 0; i < cnt; i++) {
out[i] = ares_strdup(((const ares_htable_dict_bucket_t *)buckets[i])->key);
if (out[i] == NULL) {
goto fail;
}
}
ares_free(buckets);
*num = cnt;
return out;
fail:
*num = 0;
ares_free_array(out, cnt, ares_free);
return NULL;
}

@ -27,7 +27,7 @@
#define __ARES__BUF_H #define __ARES__BUF_H
#include "ares.h" #include "ares.h"
#include "ares_llist.h" #include "ares_array.h"
/*! \addtogroup ares_buf Safe Data Builder and buffer /*! \addtogroup ares_buf Safe Data Builder and buffer
* *
@ -279,18 +279,44 @@ CARES_EXTERN ares_status_t ares_buf_tag_fetch_bytes(const ares_buf_t *buf,
/*! Fetch the bytes starting from the tagged position up to the _current_ /*! Fetch the bytes starting from the tagged position up to the _current_
* position as a NULL-terminated string using the provided buffer. The data * position as a NULL-terminated string using the provided buffer. The data
* is validated to be ASCII-printable data. It will not unset the tagged * is validated to be ASCII-printable data. It will not unset the tagged
* poition. * position.
* *
* \param[in] buf Initialized buffer object * \param[in] buf Initialized buffer object
* \param[in,out] str Buffer to hold data * \param[in,out] str Buffer to hold data
* \param[in] len On input, buffer size, on output, bytes place in * \param[in] len buffer size
* buffer.
* \return ARES_SUCCESS if fetched, ARES_EFORMERR if insufficient buffer size, * \return ARES_SUCCESS if fetched, ARES_EFORMERR if insufficient buffer size,
* ARES_EBADSTR if not printable ASCII * ARES_EBADSTR if not printable ASCII
*/ */
CARES_EXTERN ares_status_t ares_buf_tag_fetch_string(const ares_buf_t *buf, CARES_EXTERN ares_status_t ares_buf_tag_fetch_string(const ares_buf_t *buf,
char *str, size_t len); char *str, size_t len);
/*! Fetch the bytes starting from the tagged position up to the _current_
* position as a NULL-terminated string and placed into a newly allocated
* buffer. The data is validated to be ASCII-printable data. It will not
* unset the tagged position.
*
* \param[in] buf Initialized buffer object
* \param[out] str New buffer to hold output, free with ares_free()
*
* \return ARES_SUCCESS if fetched, ARES_EFORMERR if insufficient buffer size,
* ARES_EBADSTR if not printable ASCII
*/
CARES_EXTERN ares_status_t ares_buf_tag_fetch_strdup(const ares_buf_t *buf,
char **str);
/*! Fetch the bytes starting from the tagged position up to the _current_
* position as const buffer. Care must be taken to not append or destroy the
* passed in buffer until the newly fetched buffer is no longer needed since
* it points to memory inside the passed in buffer which could be invalidated.
*
* \param[in] buf Initialized buffer object
* \param[out] newbuf New const buffer object, must be destroyed when done.
* \return ARES_SUCCESS if fetched
*/
CARES_EXTERN ares_status_t ares_buf_tag_fetch_constbuf(const ares_buf_t *buf,
ares_buf_t **newbuf);
/*! Consume the given number of bytes without reading them. /*! Consume the given number of bytes without reading them.
* *
* \param[in] buf Initialized buffer object * \param[in] buf Initialized buffer object
@ -399,7 +425,7 @@ CARES_EXTERN size_t ares_buf_consume_nonwhitespace(ares_buf_t *buf);
* \param[in] require_charset require we find a character from the charset. * \param[in] require_charset require we find a character from the charset.
* if ARES_FALSE it will simply consume the * if ARES_FALSE it will simply consume the
* rest of the buffer. If ARES_TRUE will return * rest of the buffer. If ARES_TRUE will return
* 0 if not found. * SIZE_MAX if not found.
* \return number of characters consumed * \return number of characters consumed
*/ */
CARES_EXTERN size_t ares_buf_consume_until_charset(ares_buf_t *buf, CARES_EXTERN size_t ares_buf_consume_until_charset(ares_buf_t *buf,
@ -408,6 +434,23 @@ CARES_EXTERN size_t ares_buf_consume_until_charset(ares_buf_t *b
ares_bool_t require_charset); ares_bool_t require_charset);
/*! Consume until a sequence of bytes is encountered. Does not include the
* sequence of characters itself.
*
* \param[in] buf Initialized buffer object
* \param[in] seq sequence of bytes
* \param[in] len length of sequence
* \param[in] require_charset require we find the sequence.
* if ARES_FALSE it will simply consume the
* rest of the buffer. If ARES_TRUE will return
* SIZE_MAX if not found.
* \return number of characters consumed
*/
CARES_EXTERN size_t ares_buf_consume_until_seq(ares_buf_t *buf,
const unsigned char *seq,
size_t len,
ares_bool_t require_seq);
/*! Consume while the characters match the characters in the provided set. /*! Consume while the characters match the characters in the provided set.
* *
* \param[in] buf Initialized buffer object * \param[in] buf Initialized buffer object
@ -470,16 +513,39 @@ typedef enum {
* character in the value. A value of 1 would * character in the value. A value of 1 would
* have little usefulness and would effectively * have little usefulness and would effectively
* ignore the delimiter itself. * ignore the delimiter itself.
* \param[out] list Result. Depending on flags, this may be a * \param[out] arr Result. Depending on flags, this may be a
* valid list with no elements. Use * valid array with no elements. Use
* ares_llist_destroy() to free the memory which * ares_array_destroy() to free the memory which
* will also free the contained ares_buf_t * will also free the contained ares_buf_t *
* objects. * objects. Each buf object returned by
* ares_array_at() or similar is a pointer to
* an ares_buf_t * object, meaning you need to
* accept it as "ares_buf_t **" then dereference.
* \return ARES_SUCCESS on success, or error like ARES_ENOMEM. * \return ARES_SUCCESS on success, or error like ARES_ENOMEM.
*/ */
CARES_EXTERN ares_status_t ares_buf_split( CARES_EXTERN ares_status_t ares_buf_split(
ares_buf_t *buf, const unsigned char *delims, size_t delims_len, ares_buf_t *buf, const unsigned char *delims, size_t delims_len,
ares_buf_split_t flags, size_t max_sections, ares_llist_t **list); ares_buf_split_t flags, size_t max_sections, ares_array_t **arr);
/*! Split the provided buffer into an ares_array_t of C strings.
*
* \param[in] buf Initialized buffer object
* \param[in] delims Possible delimiters
* \param[in] delims_len Length of possible delimiters
* \param[in] flags One more more flags
* \param[in] max_sections Maximum number of sections. Use 0 for
* unlimited. Useful for splitting key/value
* pairs where the delimiter may be a valid
* character in the value. A value of 1 would
* have little usefulness and would effectively
* ignore the delimiter itself.
* \param[out] arr Array of strings. Free using
* ares_array_destroy().
* \return ARES_SUCCESS on success, or error like ARES_ENOMEM.
*/
CARES_EXTERN ares_status_t ares_buf_split_str_array(
ares_buf_t *buf, const unsigned char *delims, size_t delims_len,
ares_buf_split_t flags, size_t max_sections, ares_array_t **arr);
/*! Split the provided buffer into a C array of C strings. /*! Split the provided buffer into a C array of C strings.
* *
@ -533,6 +599,14 @@ CARES_EXTERN size_t ares_buf_len(const ares_buf_t *buf);
CARES_EXTERN const unsigned char *ares_buf_peek(const ares_buf_t *buf, CARES_EXTERN const unsigned char *ares_buf_peek(const ares_buf_t *buf,
size_t *len); size_t *len);
/*! Retrieve the next byte in the buffer without moving forward.
*
* \param[in] buf Initialized buffer object
* \param[out] b Single byte
* \return \return ARES_SUCCESS on success, or error
*/
CARES_EXTERN ares_status_t ares_buf_peek_byte(const ares_buf_t *buf,
unsigned char *b);
/*! Wipe any processed data from the beginning of the buffer. This will /*! Wipe any processed data from the beginning of the buffer. This will
* move any remaining data to the front of the internally allocated buffer. * move any remaining data to the front of the internally allocated buffer.

@ -0,0 +1,123 @@
/* MIT License
*
* Copyright (c) 2024 Brad House
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#ifndef __ARES__HTABLE_DICT_H
#define __ARES__HTABLE_DICT_H
/*! \addtogroup ares_htable_dict HashTable with case-insensitive string Key and
* string value
*
* This data structure wraps the base ares_htable data structure in order to
* split the key and value data types as string and string, respectively.
*
* Average time complexity:
* - Insert: O(1)
* - Search: O(1)
* - Delete: O(1)
*
* @{
*/
struct ares_htable_dict;
/*! Opaque data type for string key, string value hash table
* implementation */
typedef struct ares_htable_dict ares_htable_dict_t;
/*! Destroy hashtable
*
* \param[in] htable Initialized hashtable
*/
CARES_EXTERN void ares_htable_dict_destroy(ares_htable_dict_t *htable);
/*! Create void pointer key, string value hash table
*
*/
CARES_EXTERN ares_htable_dict_t *ares_htable_dict_create(void);
/*! Insert key/value into hash table
*
* \param[in] htable Initialized hash table
* \param[in] key key to associate with value
* \param[in] val value to store (duplicates).
* \return ARES_TRUE on success, ARES_FALSE on failure or out of memory
*/
CARES_EXTERN ares_bool_t ares_htable_dict_insert(ares_htable_dict_t *htable,
const char *key,
const char *val);
/*! Retrieve value from hashtable based on key
*
* \param[in] htable Initialized hash table
* \param[in] key key to use to search
* \param[out] val Optional. Pointer to store value.
* \return ARES_TRUE on success, ARES_FALSE on failure
*/
CARES_EXTERN ares_bool_t ares_htable_dict_get(const ares_htable_dict_t *htable,
const char *key,
const char **val);
/*! Retrieve value from hashtable directly as return value. Caveat to this
* function over ares_htable_dict_get() is that if a NULL value is stored
* you cannot determine if the key is not found or the value is NULL.
*
* \param[in] htable Initialized hash table
* \param[in] key key to use to search
* \return value associated with key in hashtable or NULL
*/
CARES_EXTERN const char *
ares_htable_dict_get_direct(const ares_htable_dict_t *htable,
const char *key);
/*! Remove a value from the hashtable by key
*
* \param[in] htable Initialized hash table
* \param[in] key key to use to search
* \return ARES_TRUE if found, ARES_FALSE if not
*/
CARES_EXTERN ares_bool_t ares_htable_dict_remove(ares_htable_dict_t *htable,
const char *key);
/*! Retrieve the number of keys stored in the hash table
*
* \param[in] htable Initialized hash table
* \return count
*/
CARES_EXTERN size_t ares_htable_dict_num_keys(const ares_htable_dict_t *htable);
/*! Retrieve an array of keys from the hashtable.
*
* \param[in] htable Initialized hashtable
* \param[out] num Count of returned keys
* \return Array of keys in the hashtable. Must be free'd with
* ares_free_array(strs, num, ares_free);
*/
CARES_EXTERN char **ares_htable_dict_keys(const ares_htable_dict_t *htable,
size_t *num);
/*! @} */
#endif /* __ARES__HTABLE_DICT_H */

@ -44,12 +44,20 @@ CARES_EXTERN size_t ares_strlen(const char *str);
CARES_EXTERN size_t ares_strcpy(char *dest, const char *src, size_t dest_size); CARES_EXTERN size_t ares_strcpy(char *dest, const char *src, size_t dest_size);
CARES_EXTERN ares_bool_t ares_str_isnum(const char *str); CARES_EXTERN ares_bool_t ares_str_isnum(const char *str);
CARES_EXTERN ares_bool_t ares_str_isalnum(const char *str);
CARES_EXTERN void ares_str_ltrim(char *str); CARES_EXTERN void ares_str_ltrim(char *str);
CARES_EXTERN void ares_str_rtrim(char *str); CARES_EXTERN void ares_str_rtrim(char *str);
CARES_EXTERN void ares_str_trim(char *str); CARES_EXTERN void ares_str_trim(char *str);
CARES_EXTERN void ares_str_lower(char *str);
CARES_EXTERN unsigned char ares_tolower(unsigned char c); CARES_EXTERN unsigned char ares_tolower(unsigned char c);
CARES_EXTERN unsigned char *ares_memmem(const unsigned char *big,
size_t big_len,
const unsigned char *little,
size_t little_len);
CARES_EXTERN ares_bool_t ares_memeq(const unsigned char *ptr,
const unsigned char *val, size_t len);
CARES_EXTERN ares_bool_t ares_memeq_ci(const unsigned char *ptr, CARES_EXTERN ares_bool_t ares_memeq_ci(const unsigned char *ptr,
const unsigned char *val, size_t len); const unsigned char *val, size_t len);
CARES_EXTERN ares_bool_t ares_is_hostname(const char *str); CARES_EXTERN ares_bool_t ares_is_hostname(const char *str);

@ -416,6 +416,23 @@ ares_status_t ares_buf_tag_fetch_bytes(const ares_buf_t *buf,
return ARES_SUCCESS; return ARES_SUCCESS;
} }
ares_status_t ares_buf_tag_fetch_constbuf(const ares_buf_t *buf,
ares_buf_t **newbuf)
{
size_t ptr_len = 0;
const unsigned char *ptr = ares_buf_tag_fetch(buf, &ptr_len);
if (ptr == NULL || newbuf == NULL) {
return ARES_EFORMERR;
}
*newbuf = ares_buf_create_const(ptr, ptr_len);
if (*newbuf == NULL) {
return ARES_ENOMEM;
}
return ARES_SUCCESS;
}
ares_status_t ares_buf_tag_fetch_string(const ares_buf_t *buf, char *str, ares_status_t ares_buf_tag_fetch_string(const ares_buf_t *buf, char *str,
size_t len) size_t len)
{ {
@ -448,6 +465,31 @@ ares_status_t ares_buf_tag_fetch_string(const ares_buf_t *buf, char *str,
return ARES_SUCCESS; return ARES_SUCCESS;
} }
ares_status_t ares_buf_tag_fetch_strdup(const ares_buf_t *buf, char **str)
{
size_t ptr_len = 0;
const unsigned char *ptr = ares_buf_tag_fetch(buf, &ptr_len);
if (ptr == NULL || str == NULL) {
return ARES_EFORMERR;
}
if (!ares_str_isprint((const char *)ptr, ptr_len)) {
return ARES_EBADSTR;
}
*str = ares_malloc(ptr_len + 1);
if (*str == NULL) {
return ARES_ENOMEM;
}
if (ptr_len > 0) {
memcpy(*str, ptr, ptr_len);
}
(*str)[ptr_len] = 0;
return ARES_SUCCESS;
}
static const unsigned char *ares_buf_fetch(const ares_buf_t *buf, size_t *len) static const unsigned char *ares_buf_fetch(const ares_buf_t *buf, size_t *len)
{ {
if (len != NULL) { if (len != NULL) {
@ -691,17 +733,29 @@ size_t ares_buf_consume_until_charset(ares_buf_t *buf,
{ {
size_t remaining_len = 0; size_t remaining_len = 0;
const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len); const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);
size_t i; size_t pos;
ares_bool_t found = ARES_FALSE; ares_bool_t found = ARES_FALSE;
if (ptr == NULL || charset == NULL || len == 0) { if (ptr == NULL || charset == NULL || len == 0) {
return 0; return 0;
} }
for (i = 0; i < remaining_len; i++) { /* Optimize for single character searches */
if (len == 1) {
const unsigned char *p = memchr(ptr, charset[0], remaining_len);
if (p != NULL) {
found = ARES_TRUE;
pos = (size_t)(p - ptr);
} else {
pos = remaining_len;
}
goto done;
}
for (pos = 0; pos < remaining_len; pos++) {
size_t j; size_t j;
for (j = 0; j < len; j++) { for (j = 0; j < len; j++) {
if (ptr[i] == charset[j]) { if (ptr[pos] == charset[j]) {
found = ARES_TRUE; found = ARES_TRUE;
goto done; goto done;
} }
@ -710,13 +764,43 @@ size_t ares_buf_consume_until_charset(ares_buf_t *buf,
done: done:
if (require_charset && !found) { if (require_charset && !found) {
return SIZE_MAX;
}
if (pos > 0) {
ares_buf_consume(buf, pos);
}
return pos;
}
size_t ares_buf_consume_until_seq(ares_buf_t *buf, const unsigned char *seq,
size_t len, ares_bool_t require_seq)
{
size_t remaining_len = 0;
const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);
const unsigned char *p;
size_t consume_len = 0;
if (ptr == NULL || seq == NULL || len == 0) {
return 0; return 0;
} }
if (i > 0) { p = ares_memmem(ptr, remaining_len, seq, len);
ares_buf_consume(buf, i); if (require_seq && p == NULL) {
return SIZE_MAX;
} }
return i;
if (p != NULL) {
consume_len = (size_t)(p - ptr);
} else {
consume_len = remaining_len;
}
if (consume_len > 0) {
ares_buf_consume(buf, consume_len);
}
return consume_len;
} }
size_t ares_buf_consume_charset(ares_buf_t *buf, const unsigned char *charset, size_t ares_buf_consume_charset(ares_buf_t *buf, const unsigned char *charset,
@ -751,19 +835,21 @@ size_t ares_buf_consume_charset(ares_buf_t *buf, const unsigned char *charset,
static void ares_buf_destroy_cb(void *arg) static void ares_buf_destroy_cb(void *arg)
{ {
ares_buf_destroy(arg); ares_buf_t **buf = arg;
ares_buf_destroy(*buf);
} }
static ares_bool_t ares_buf_split_isduplicate(ares_llist_t *list, static ares_bool_t ares_buf_split_isduplicate(ares_array_t *arr,
const unsigned char *val, const unsigned char *val,
size_t len, size_t len,
ares_buf_split_t flags) ares_buf_split_t flags)
{ {
ares_llist_node_t *node; size_t i;
size_t num = ares_array_len(arr);
for (node = ares_llist_node_first(list); node != NULL; for (i = 0; i < num; i++) {
node = ares_llist_node_next(node)) { ares_buf_t **bufptr = ares_array_at(arr, i);
const ares_buf_t *buf = ares_llist_node_val(node); ares_buf_t *buf = *bufptr;
size_t plen = 0; size_t plen = 0;
const unsigned char *ptr = ares_buf_peek(buf, &plen); const unsigned char *ptr = ares_buf_peek(buf, &plen);
@ -777,27 +863,28 @@ static ares_bool_t ares_buf_split_isduplicate(ares_llist_t *list,
return ARES_TRUE; return ARES_TRUE;
} }
} else { } else {
if (memcmp(ptr, val, len) == 0) { if (ares_memeq(ptr, val, len)) {
return ARES_TRUE; return ARES_TRUE;
} }
} }
} }
return ARES_FALSE; return ARES_FALSE;
} }
ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims, ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims,
size_t delims_len, ares_buf_split_t flags, size_t delims_len, ares_buf_split_t flags,
size_t max_sections, ares_llist_t **list) size_t max_sections, ares_array_t **arr)
{ {
ares_status_t status = ARES_SUCCESS; ares_status_t status = ARES_SUCCESS;
ares_bool_t first = ARES_TRUE; ares_bool_t first = ARES_TRUE;
if (buf == NULL || delims == NULL || delims_len == 0 || list == NULL) { if (buf == NULL || delims == NULL || delims_len == 0 || arr == NULL) {
return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
} }
*list = ares_llist_create(ares_buf_destroy_cb); *arr = ares_array_create(sizeof(ares_buf_t *), ares_buf_destroy_cb);
if (*list == NULL) { if (*arr == NULL) {
status = ARES_ENOMEM; status = ARES_ENOMEM;
goto done; goto done;
} }
@ -821,7 +908,7 @@ ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims,
} }
} }
if (max_sections && ares_llist_len(*list) >= max_sections - 1) { if (max_sections && ares_array_len(*arr) >= max_sections - 1) {
ares_buf_consume(buf, ares_buf_len(buf)); ares_buf_consume(buf, ares_buf_len(buf));
} else { } else {
ares_buf_consume_until_charset(buf, delims, delims_len, ARES_FALSE); ares_buf_consume_until_charset(buf, delims, delims_len, ARES_FALSE);
@ -856,7 +943,7 @@ ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims,
ares_buf_t *data; ares_buf_t *data;
if (!(flags & ARES_BUF_SPLIT_NO_DUPLICATES) || if (!(flags & ARES_BUF_SPLIT_NO_DUPLICATES) ||
!ares_buf_split_isduplicate(*list, ptr, len, flags)) { !ares_buf_split_isduplicate(*arr, ptr, len, flags)) {
/* Since we don't allow const buffers of 0 length, and user wants /* Since we don't allow const buffers of 0 length, and user wants
* 0-length buffers, swap what we do here */ * 0-length buffers, swap what we do here */
if (len) { if (len) {
@ -870,9 +957,9 @@ ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims,
goto done; goto done;
} }
if (ares_llist_insert_last(*list, data) == NULL) { status = ares_array_insertdata_last(*arr, &data);
if (status != ARES_SUCCESS) {
ares_buf_destroy(data); ares_buf_destroy(data);
status = ARES_ENOMEM;
goto done; goto done;
} }
} }
@ -883,8 +970,8 @@ ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims,
done: done:
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
ares_llist_destroy(*list); ares_array_destroy(*arr);
*list = NULL; *arr = NULL;
} }
return status; return status;
@ -896,37 +983,38 @@ static void ares_free_split_array(void *arg)
ares_free(*ptr); ares_free(*ptr);
} }
ares_status_t ares_buf_split_str(ares_buf_t *buf, const unsigned char *delims, ares_status_t ares_buf_split_str_array(ares_buf_t *buf,
size_t delims_len, ares_buf_split_t flags, const unsigned char *delims,
size_t max_sections, char ***strs, size_t delims_len,
size_t *nstrs) ares_buf_split_t flags,
size_t max_sections, ares_array_t **arr)
{ {
ares_status_t status; ares_status_t status;
ares_llist_t *list = NULL; ares_array_t *split = NULL;
ares_llist_node_t *node; size_t i;
ares_array_t *arr = NULL; size_t len;
if (strs == NULL || nstrs == NULL) { if (arr == NULL) {
return ARES_EFORMERR; return ARES_EFORMERR;
} }
*strs = NULL; *arr = NULL;
*nstrs = 0;
status = ares_buf_split(buf, delims, delims_len, flags, max_sections, &list); status = ares_buf_split(buf, delims, delims_len, flags, max_sections, &split);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
} }
arr = ares_array_create(sizeof(char *), ares_free_split_array); *arr = ares_array_create(sizeof(char *), ares_free_split_array);
if (arr == NULL) { if (*arr == NULL) {
status = ARES_ENOMEM; status = ARES_ENOMEM;
goto done; goto done;
} }
for (node = ares_llist_node_first(list); node != NULL; len = ares_array_len(split);
node = ares_llist_node_next(node)) { for (i = 0; i < len; i++) {
ares_buf_t *lbuf = ares_llist_node_val(node); ares_buf_t **bufptr = ares_array_at(split, i);
ares_buf_t *lbuf = *bufptr;
char *str = NULL; char *str = NULL;
status = ares_buf_fetch_str_dup(lbuf, ares_buf_len(lbuf), &str); status = ares_buf_fetch_str_dup(lbuf, ares_buf_len(lbuf), &str);
@ -934,7 +1022,7 @@ ares_status_t ares_buf_split_str(ares_buf_t *buf, const unsigned char *delims,
goto done; goto done;
} }
status = ares_array_insertdata_last(arr, &str); status = ares_array_insertdata_last(*arr, &str);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
ares_free(str); ares_free(str);
goto done; goto done;
@ -942,7 +1030,37 @@ ares_status_t ares_buf_split_str(ares_buf_t *buf, const unsigned char *delims,
} }
done: done:
ares_llist_destroy(list); ares_array_destroy(split);
if (status != ARES_SUCCESS) {
ares_array_destroy(*arr);
*arr = NULL;
}
return status;
}
ares_status_t ares_buf_split_str(ares_buf_t *buf, const unsigned char *delims,
size_t delims_len, ares_buf_split_t flags,
size_t max_sections, char ***strs,
size_t *nstrs)
{
ares_status_t status;
ares_array_t *arr = NULL;
if (strs == NULL || nstrs == NULL) {
return ARES_EFORMERR;
}
*strs = NULL;
*nstrs = 0;
status = ares_buf_split_str_array(buf, delims, delims_len, flags,
max_sections, &arr);
if (status != ARES_SUCCESS) {
goto done;
}
done:
if (status == ARES_SUCCESS) { if (status == ARES_SUCCESS) {
*strs = ares_array_finish(arr, nstrs); *strs = ares_array_finish(arr, nstrs);
} else { } else {
@ -986,6 +1104,22 @@ const unsigned char *ares_buf_peek(const ares_buf_t *buf, size_t *len)
return ares_buf_fetch(buf, len); return ares_buf_fetch(buf, len);
} }
ares_status_t ares_buf_peek_byte(const ares_buf_t *buf, unsigned char *b)
{
size_t remaining_len = 0;
const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);
if (buf == NULL || b == NULL) {
return ARES_EFORMERR;
}
if (remaining_len == 0) {
return ARES_EBADRESP;
}
*b = ptr[0];
return ARES_SUCCESS;
}
size_t ares_buf_get_position(const ares_buf_t *buf) size_t ares_buf_get_position(const ares_buf_t *buf)
{ {
if (buf == NULL) { if (buf == NULL) {

@ -101,7 +101,23 @@ ares_bool_t ares_str_isnum(const char *str)
} }
for (i = 0; str[i] != 0; i++) { for (i = 0; str[i] != 0; i++) {
if (str[i] < '0' || str[i] > '9') { if (!ares_isdigit(str[i])) {
return ARES_FALSE;
}
}
return ARES_TRUE;
}
ares_bool_t ares_str_isalnum(const char *str)
{
size_t i;
if (str == NULL || *str == 0) {
return ARES_FALSE;
}
for (i = 0; str[i] != 0; i++) {
if (!ares_isdigit(str[i]) && !ares_isalpha(str[i])) {
return ARES_FALSE; return ARES_FALSE;
} }
} }
@ -186,6 +202,62 @@ unsigned char ares_tolower(unsigned char c)
return ares_tolower_lookup[c]; return ares_tolower_lookup[c];
} }
void ares_str_lower(char *str)
{
size_t i;
if (str == NULL) {
return;
}
for (i = 0; str[i] != 0; i++) {
str[i] = (char)ares_tolower((unsigned char)str[i]);
}
}
unsigned char *ares_memmem(const unsigned char *big, size_t big_len,
const unsigned char *little, size_t little_len)
{
unsigned char *ptr;
if (big == NULL || little == NULL || big_len == 0 || little_len == 0) {
return NULL;
}
#ifdef HAVE_MEMMEM
ptr = memmem(big, big_len, little, little_len);
return ptr;
#else
while (1) {
ptr = memchr(big, little[0], big_len);
if (ptr == NULL) {
break;
}
big_len -= (size_t)(ptr - big);
big = ptr;
if (big_len < little_len) {
break;
}
if (memcmp(big, little, little_len) == 0) {
return ptr;
}
big++;
big_len--;
}
return NULL;
#endif
}
ares_bool_t ares_memeq(const unsigned char *ptr, const unsigned char *val,
size_t len)
{
return memcmp(ptr, val, len) == 0 ? ARES_TRUE : ARES_FALSE;
}
ares_bool_t ares_memeq_ci(const unsigned char *ptr, const unsigned char *val, ares_bool_t ares_memeq_ci(const unsigned char *ptr, const unsigned char *val,
size_t len) size_t len)
{ {

@ -59,11 +59,7 @@ char **ares_strsplit(const char *in, const char *delms, size_t *num_elm)
{ {
ares_status_t status; ares_status_t status;
ares_buf_t *buf = NULL; ares_buf_t *buf = NULL;
ares_llist_t *llist = NULL;
ares_llist_node_t *node;
char **out = NULL; char **out = NULL;
size_t cnt = 0;
size_t idx = 0;
if (in == NULL || delms == NULL || num_elm == NULL) { if (in == NULL || delms == NULL || num_elm == NULL) {
return NULL; /* LCOV_EXCL_LINE: DefensiveCoding */ return NULL; /* LCOV_EXCL_LINE: DefensiveCoding */
@ -76,47 +72,17 @@ char **ares_strsplit(const char *in, const char *delms, size_t *num_elm)
return NULL; return NULL;
} }
status = ares_buf_split( status = ares_buf_split_str(
buf, (const unsigned char *)delms, ares_strlen(delms), buf, (const unsigned char *)delms, ares_strlen(delms),
ARES_BUF_SPLIT_NO_DUPLICATES | ARES_BUF_SPLIT_CASE_INSENSITIVE, 0, &llist); ARES_BUF_SPLIT_NO_DUPLICATES | ARES_BUF_SPLIT_CASE_INSENSITIVE, 0, &out,
num_elm);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;
} }
cnt = ares_llist_len(llist);
if (cnt == 0) {
status = ARES_EFORMERR;
goto done;
}
out = ares_malloc_zero(cnt * sizeof(*out));
if (out == NULL) {
status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
goto done; /* LCOV_EXCL_LINE: OutOfMemory */
}
for (node = ares_llist_node_first(llist); node != NULL;
node = ares_llist_node_next(node)) {
ares_buf_t *val = ares_llist_node_val(node);
char *temp = NULL;
status = ares_buf_fetch_str_dup(val, ares_buf_len(val), &temp);
if (status != ARES_SUCCESS) {
goto done;
}
out[idx++] = temp;
}
*num_elm = cnt;
status = ARES_SUCCESS;
done: done:
ares_llist_destroy(llist);
ares_buf_destroy(buf); ares_buf_destroy(buf);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
ares_strsplit_free(out, cnt);
out = NULL; out = NULL;
} }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,252 @@
/* MIT License
*
* Copyright (c) 2024 Brad House
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#ifndef __ARES_URI_H
#define __ARES_URI_H
/*! \addtogroup ares_uri URI parser and writer implementation
*
* This is a fairly complete URI parser and writer implementation (RFC 3986) for
* schemes which use the :// syntax. Does not currently support URIs without an
* authority section, such as "mailto:person@example.com".
*
* Its implementation is overkill for our current needs to be able to express
* DNS server configuration, but there was really no reason not to support
* a greater subset of the specification.
*
* @{
*/
struct ares_uri;
/*! URI object */
typedef struct ares_uri ares_uri_t;
/*! Create a new URI object
*
* \return new ares_uri_t, must be freed with ares_uri_destroy()
*/
ares_uri_t *ares_uri_create(void);
/*! Destroy an initialized URI object
*
* \param[in] uri Initialized URI object
*/
void ares_uri_destroy(ares_uri_t *uri);
/*! Set the URI scheme. Automatically lower-cases the scheme provided.
* Only allows Alpha, Digit, +, -, and . characters. Maximum length is
* 15 characters. This is required to be set to write a URI.
*
* \param[in] uri Initialized URI object
* \param[in] scheme Scheme to set the object to use
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_scheme(ares_uri_t *uri, const char *scheme);
/*! Retrieve the currently configured URI scheme.
*
* \param[in] uri Initialized URI object
* \return string containing URI scheme
*/
const char *ares_uri_get_scheme(ares_uri_t *uri);
/*! Set the username in the URI object
*
* \param[in] uri Initialized URI object
* \param[in] username Username to set. May be NULL to unset existing username.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_username(ares_uri_t *uri, const char *username);
/*! Retrieve the currently configured username.
*
* \param[in] uri Initialized URI object
* \return string containing username, maybe NULL if not set.
*/
const char *ares_uri_get_username(ares_uri_t *uri);
/*! Set the password in the URI object
*
* \param[in] uri Initialized URI object
* \param[in] password Password to set. May be NULL to unset existing password.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_password(ares_uri_t *uri, const char *password);
/*! Retrieve the currently configured password.
*
* \param[in] uri Initialized URI object
* \return string containing password, maybe NULL if not set.
*/
const char *ares_uri_get_password(ares_uri_t *uri);
/*! Set the host or ip address in the URI object. This is required to be
* set to write a URI. The character set is strictly validated.
*
* \param[in] uri Initialized URI object
* \param[in] host IPv4, IPv6, or hostname to set.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_host(ares_uri_t *uri, const char *host);
/*! Retrieve the currently configured host (or ip address). IPv6 addresses
* May include a link-local scope (e.g. fe80::b542:84df:1719:65e3%en0).
*
* \param[in] uri Initialized URI object
* \return string containing host, maybe NULL if not set.
*/
const char *ares_uri_get_host(ares_uri_t *uri);
/*! Set the port to use in the URI object. A port value of 0 will omit
* the port from the URI when written, thus using the scheme's default.
*
* \param[in] uri Initialized URI object
* \param[in] port Port to set. Use 0 to unset.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_port(ares_uri_t *uri, unsigned short port);
/*! Retrieve the currently configured port
*
* \param[in] uri Initialized URI object
* \return port number, or 0 if not set.
*/
unsigned short ares_uri_get_port(ares_uri_t *uri);
/*! Set the path in the URI object. Unsupported characters will be URI-encoded
* when written.
*
* \param[in] uri Initialized URI object
* \param[in] path Path to set. May be NULL to unset existing path.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_path(ares_uri_t *uri, const char *path);
/*! Retrieves the path in the URI object. If retrieved after parse, this
* value will be URI-decoded already.
*
* \param[in] uri Initialized URI object
* \return path string, or NULL if not set.
*/
const char *ares_uri_get_path(ares_uri_t *uri);
/*! Set a new query key/value pair. There is no set order for query keys
* when output in the URI, they will be emitted in a random order. Keys are
* case-insensitive. Query keys and values will be automatically URI-encoded
* when written.
*
* \param[in] uri Initialized URI object
* \param[in] key Query key to use, must be non-zero length.
* \param[in] val Query value to use, may be NULL.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_query_key(ares_uri_t *uri, const char *key,
const char *val);
/*! Delete a specific query key.
*
* \param[in] uri Initialized URI object
* \param[in] key Key to delete.
* \return ARES_SUCCESS if deleted, ARES_ENOTFOUND if not found
*/
ares_status_t ares_uri_del_query_key(ares_uri_t *uri, const char *key);
/*! Retrieve the value associted with a query key. Keys are case-insensitive.
*
* \param[in] uri Initialized URI object
* \param[in] key Key to retrieve.
* \return string representing value, may be NULL if either not found or
* NULL value set. There is currently no way to indicate the
* difference.
*/
const char *ares_uri_get_query_key(ares_uri_t *uri, const char *key);
/*! Retrieve a complete list of query keys.
*
* \param[in] uri Initialized URI object
* \param[out] num Number of keys.
* \return NULL on failure or no keys. Use
* ares_free_array(keys, num, ares_free) when done with array.
*/
char **ares_uri_get_query_keys(ares_uri_t *uri, size_t *num);
/*! Set the fragment in the URI object. Unsupported characters will be
* URI-encoded when written.
*
* \param[in] uri Initialized URI object
* \param[in] fragment Fragment to set. May be NULL to unset existing fragment.
* \return ARES_SUCCESS on success
*/
ares_status_t ares_uri_set_fragment(ares_uri_t *uri, const char *fragment);
/*! Retrieves the fragment in the URI object. If retrieved after parse, this
* value will be URI-decoded already.
*
* \param[in] uri Initialized URI object
* \return fragment string, or NULL if not set.
*/
const char *ares_uri_get_fragment(ares_uri_t *uri);
/*! Parse the provided URI buffer into a new URI object.
*
* \param[out] out Returned new URI object. free with ares_uri_destroy().
* \param[in] buf Buffer object containing the URI
* \return ARES_SUCCESS on successful parse. On failure the 'buf' object will
* be restored to its initial state in case another parser needs to
* be attempted.
*/
ares_status_t ares_uri_parse_buf(ares_uri_t **out, ares_buf_t *buf);
/*! Parse the provided URI string into a new URI object.
*
* \param[out] out Returned new URI object. free with ares_uri_destroy().
* \param[in] uri URI string to parse
* \return ARES_SUCCESS on successful parse
*/
ares_status_t ares_uri_parse(ares_uri_t **out, const char *uri);
/*! Write URI object to a new string buffer. Requires at least the scheme
* and host to be set for this to succeed.
*
* \param[out] out Returned new URI string. Free with ares_free().
* \param[in] uri Initialized URI object.
* \return ARES_SUCCESS on successful write.
*/
ares_status_t ares_uri_write(char **out, ares_uri_t *uri);
/*! Write URI object to an existing ares_buf_t object. Requires at least the
* scheme and host to be set for this to succeed.
*
* \param[in] uri Initialized URI object.
* \param[in,out] buf Destination buf object.
* \return ARES_SUCCESS on successful write.
*/
ares_status_t ares_uri_write_buf(ares_uri_t *uri, ares_buf_t *buf);
/*! @} */
#endif /* __ARES_URI_H */

@ -33,6 +33,11 @@
int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size); int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size);
/* Fuzzing on a query name isn't very useful as its already fuzzed as part
* of the normal fuzzing operations. So we'll disable this by default and
* instead use this same fuzzer to validate our URI scheme parsers accessed
* via ares_set_servers_csv() */
#ifdef USE_LEGACY_FUZZERS
/* Entrypoint for Clang's libfuzzer, exercising query creation. */ /* Entrypoint for Clang's libfuzzer, exercising query creation. */
int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size) int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size)
{ {
@ -48,3 +53,27 @@ int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size)
free(name); free(name);
return 0; return 0;
} }
#else
int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size)
{
ares_channel_t *channel = NULL;
char *csv;
ares_library_init(ARES_LIB_INIT_ALL);
ares_init(&channel);
/* Need to null-term data */
csv = malloc(size + 1);
memcpy(csv, data, size);
csv[size] = '\0';
ares_set_servers_csv(channel, csv);
free(csv);
ares_destroy(channel);
ares_library_cleanup();
return 0;
}
#endif

@ -31,7 +31,7 @@
int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size); int LLVMFuzzerTestOneInput(const unsigned char *data, unsigned long size);
#ifdef USE_LEGACY_PARSERS #ifdef USE_LEGACY_FUZZERS
/* This implementation calls the legacy c-ares parsers, which historically /* This implementation calls the legacy c-ares parsers, which historically
* all used different logic and parsing. As of c-ares 1.21.0 these are * all used different logic and parsing. As of c-ares 1.21.0 these are

@ -340,6 +340,83 @@ TEST_F(LibraryTest, HtableMisuse) {
EXPECT_EQ(ARES_FALSE, ares_htable_remove(NULL, NULL)); EXPECT_EQ(ARES_FALSE, ares_htable_remove(NULL, NULL));
EXPECT_EQ((size_t)0, ares_htable_num_keys(NULL)); EXPECT_EQ((size_t)0, ares_htable_num_keys(NULL));
} }
TEST_F(LibraryTest, URI) {
struct {
ares_bool_t success;
const char *uri;
const char *alt_match_uri;
} tests[] = {
{ ARES_TRUE, "https://www.example.com", NULL },
{ ARES_TRUE, "https://www.example.com:8443", NULL },
{ ARES_TRUE, "https://user:password@www.example.com", NULL },
{ ARES_TRUE, "https://user%25:password@www.example.com", NULL },
{ ARES_TRUE, "https://user:password%25@www.example.com", NULL },
{ ARES_TRUE, "https://user@www.example.com", NULL },
{ ARES_TRUE, "https://www.example.com/path", NULL },
{ ARES_TRUE, "https://www.example.com/path/", NULL },
{ ARES_TRUE, "https://www.example.com/a/../", "https://www.example.com/" },
{ ARES_TRUE, "https://www.example.com/../a/", "https://www.example.com/a/" },
{ ARES_TRUE, "https://www.example.com/.././../a/", "https://www.example.com/a/" },
{ ARES_TRUE, "https://www.example.com/.././../a//b/c/d/../../", "https://www.example.com/a/b/" },
{ ARES_TRUE, "https://www.example.com?key=val", NULL },
{ ARES_TRUE, "https://www.example.com?key", NULL },
{ ARES_TRUE, "https://www.example.com?key=", "https://www.example.com?key" },
{ ARES_TRUE, "https://www.example.com#fragment", NULL },
{ ARES_TRUE, "https://user:password@www.example.com/path", NULL },
{ ARES_TRUE, "https://user:password@www.example.com/path#fragment", NULL },
{ ARES_TRUE, "https://user:password@www.example.com/path?key=val", NULL },
{ ARES_TRUE, "https://user:password@www.example.com/path?key=val#fragment", NULL },
{ ARES_TRUE, "HTTPS://www.example.com", "https://www.example.com" },
{ ARES_TRUE, "https://www.example.com?key=hello+world", "https://www.example.com?key=hello%20world" },
{ ARES_TRUE, "https://www.example.com?key=val%26", NULL },
{ ARES_TRUE, "https://www.example.com?key%26=val", NULL },
{ ARES_TRUE, "https://www.example.com?key=Aa2-._~/?!$'()*,;:@", NULL },
{ ARES_TRUE, "https://www.example.com?key1=val1&key2=val2&key3=val3&key4=val4", "ignore" }, /* keys get randomized, can't match */
{ ARES_TRUE, "https://www.example.com?key=%41%61%32%2D%2E%5f%7e%2F%3F%21%24%27%28%29%2a%2C%3b%3a%40", "https://www.example.com?key=Aa2-._~/?!$'()*,;:@" },
{ ARES_TRUE, "dns+tls://192.168.1.1:53", NULL },
{ ARES_TRUE, "dns+tls://[fe80::1]:53", NULL },
{ ARES_TRUE, "dns://[fe80::b542:84df:1719:65e3%en0]", NULL },
{ ARES_TRUE, "dns+tls://[fe80:00::00:1]:53", "dns+tls://[fe80::1]:53" },
{ ARES_TRUE, "d.n+s-tls://www.example.com", NULL },
{ ARES_FALSE, "dns*tls://www.example.com", NULL }, /* invalid scheme character */
{ ARES_FALSE, "https://www.example.com?key=val%01", NULL }, /* non-printable character */
{ ARES_FALSE, "abcdef0123456789://www.example.com", NULL }, /* scheme too long */
{ ARES_FALSE, "www.example.com", NULL }, /* missing scheme */
{ ARES_FALSE, "https://www.example.com?key=val%0", NULL }, /* truncated uri-encoding */
{ ARES_FALSE, "https://www.example.com?key=val%AZ", NULL }, /* invalid uri-encoding sequence */
{ ARES_FALSE, "https://www.example.com?key=hello world", NULL }, /* invalid character in query value */
{ ARES_FALSE, "https://:password@www.example.com", NULL }, /* can't have password without username */
{ ARES_FALSE, "dns+tls://[fe8G::1]", NULL }, /* invalid ipv6 address */
{ ARES_FALSE, NULL, NULL }
};
size_t i;
for (i=0; tests[i].uri != NULL; i++) {
ares_uri_t *uri = NULL;
ares_status_t status;
if (verbose) std::cerr << "Testing " << tests[i].uri << std::endl;
status = ares_uri_parse(&uri, tests[i].uri);
if (tests[i].success) {
EXPECT_EQ(ARES_SUCCESS, status);
} else {
EXPECT_NE(ARES_SUCCESS, status);
}
if (status == ARES_SUCCESS) {
char *out = NULL;
EXPECT_EQ(ARES_SUCCESS, ares_uri_write(&out, uri));
if (tests[i].alt_match_uri == NULL || strcmp(tests[i].alt_match_uri, "ignore") != 0) {
EXPECT_STRCASEEQ(tests[i].alt_match_uri == NULL?tests[i].uri:tests[i].alt_match_uri, out);
}
ares_free(out);
}
ares_uri_destroy(uri);
}
}
#endif /* !CARES_SYMBOL_HIDING */ #endif /* !CARES_SYMBOL_HIDING */
TEST_F(LibraryTest, InetPtoN) { TEST_F(LibraryTest, InetPtoN) {

@ -0,0 +1 @@
https://www.example.com

@ -0,0 +1 @@
https://www.example.com?key=

@ -0,0 +1 @@
https://www.example.com#fragment

@ -0,0 +1 @@
https://user:password@www.example.com/path

@ -0,0 +1 @@
https://user:password@www.example.com/path#fragment

@ -0,0 +1 @@
https://user:password@www.example.com/path?key=val

@ -0,0 +1 @@
https://user:password@www.example.com/path?key=val#fragment

@ -0,0 +1 @@
HTTPS://www.example.com

@ -0,0 +1 @@
https://www.example.com?key=hello+world

@ -0,0 +1 @@
https://www.example.com?key=val%26

@ -0,0 +1 @@
https://www.example.com:8443

@ -0,0 +1 @@
https://www.example.com?key%26=val

@ -0,0 +1 @@
https://www.example.com?key=Aa2-._~/?uri20\'\(\)\*,;:@

@ -0,0 +1 @@
https://www.example.com?key1=val1&key2=val2&key3=val3&key4=val4

@ -0,0 +1 @@
https://www.example.com?key=%41%61%32%2D%2E%5f%7e%2F%3F%21%24%27%28%29%2a%2C%3b%3a%40

@ -0,0 +1 @@
dns+tls://192.168.1.1:53

@ -0,0 +1 @@
dns+tls://[fe80::1]:53

@ -0,0 +1 @@
dns://[fe80::b542:84df:1719:65e3%en0]

@ -0,0 +1 @@
dns+tls://[fe80:00::00:1]:53

@ -0,0 +1 @@
d.n+s-tls://www.example.com

@ -0,0 +1 @@
dns*tls://www.example.com

@ -0,0 +1 @@
https://user:password@www.example.com

@ -0,0 +1 @@
https://www.example.com?key=val%01

@ -0,0 +1 @@
abcdef0123456789://www.example.com

@ -0,0 +1 @@
www.example.com

@ -0,0 +1 @@
https://www.example.com?key=val%0

@ -0,0 +1 @@
https://www.example.com?key=val%AZ

@ -0,0 +1 @@
https://www.example.com?key=hello world

@ -0,0 +1 @@
https://:password@www.example.com

@ -0,0 +1 @@
dns+tls://[fe8G::1]

@ -0,0 +1 @@
https://user%25:password@www.example.com

@ -0,0 +1 @@
https://user:password%25@www.example.com

@ -0,0 +1 @@
https://user@www.example.com

@ -0,0 +1 @@
https://www.example.com/path

@ -0,0 +1 @@
https://www.example.com?key=val

@ -0,0 +1 @@
https://www.example.com?key
Loading…
Cancel
Save