diff options
author | Patrick Griffis <pgriffis@igalia.com> | 2021-04-19 12:17:58 -0500 |
---|---|---|
committer | Patrick Griffis <pgriffis@igalia.com> | 2021-12-22 22:23:00 -0600 |
commit | e11e4e34c94a8da65eee03eca89380f135173b89 (patch) | |
tree | 7d1ea435354fecfa91bdda32cc47ac3a46a8a8ad | |
parent | eaa52ddfc5b201377d5df3ef5b735396c5433bf3 (diff) | |
download | glib-pgriffis/wip/resolver-https.tar.gz |
gresolver: Add support for the HTTPS DNS typepgriffis/wip/resolver-https
This is a new type used to request information about an HTTPS
service and contains information like alternative hosts, ports,
EncryptedClientHello keys, and protocols supported.
-rw-r--r-- | gio/gioenums.h | 17 | ||||
-rw-r--r-- | gio/gthreadedresolver.c | 351 | ||||
-rw-r--r-- | gio/meson.build | 8 | ||||
-rw-r--r-- | gio/tests/gresolver.c | 347 | ||||
-rw-r--r-- | gio/tests/meson.build | 1 | ||||
-rw-r--r-- | gio/tests/resolver.c | 58 |
6 files changed, 778 insertions, 4 deletions
diff --git a/gio/gioenums.h b/gio/gioenums.h index 4c595803d..4e097624e 100644 --- a/gio/gioenums.h +++ b/gio/gioenums.h @@ -722,6 +722,7 @@ typedef enum { * @G_RESOLVER_RECORD_TXT: look up DNS TXT records for a name * @G_RESOLVER_RECORD_SOA: look up DNS SOA records for a zone * @G_RESOLVER_RECORD_NS: look up DNS NS records for a domain + * @G_RESOLVER_RECORD_HTTPS: look up DNS HTTPS records for a zone. Since: 2.72 * * The type of record that g_resolver_lookup_records() or * g_resolver_lookup_records_async() should retrieve. The records are returned @@ -754,6 +755,19 @@ typedef enum { * %G_RESOLVER_RECORD_NS records are returned as variants with the signature * `(s)`, representing a string of the hostname of the name server. * + * %G_RESOLVER_RECORD_HTTPS records are returned as variants with the signature + * `(qsa{sv})`, representing the priority, target host, and params of the domain. + * The keys of the params dictionary are: + * - `alpn`: array of strings of protocol names + * - `no-default-alpn`: an empty string if present + * - `port`: uint16 (0-65535) + * - `ipv4hint`: array of strings of addresses + * - `ipv6hint`: array of strings of addresses + * - `ech`: byte array of data containing an ECHConfigList defined [here](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/) + * - `mandatory`: array of strings matching these keys + * - `keyN`: for unknown keys, N is the numeric value of the key type, the value is a byte array of unparsed data + * See the [RFC](https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/) for more information. + * * Since: 2.34 */ typedef enum { @@ -761,7 +775,8 @@ typedef enum { G_RESOLVER_RECORD_MX, G_RESOLVER_RECORD_TXT, G_RESOLVER_RECORD_SOA, - G_RESOLVER_RECORD_NS + G_RESOLVER_RECORD_NS, + G_RESOLVER_RECORD_HTTPS } GResolverRecordType; /** diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 48545d6ad..4c9fdf60f 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -394,6 +394,10 @@ lookup_by_address_finish (GResolver *resolver, #if defined(G_OS_UNIX) +#ifndef T_HTTPS +#define T_HTTPS 65 +#endif + #if defined __BIONIC__ && !defined BIND_4_COMPAT /* Copy from bionic/libc/private/arpa_nameser_compat.h * and bionic/libc/private/arpa_nameser.h */ @@ -633,6 +637,330 @@ parse_res_txt (const guint8 *answer, return record; } +/* Defined in Section 6 (https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/) */ +typedef enum { + SVCB_KEY_MANDATORY = 0, + SVCB_KEY_ALPN, + SVCB_KEY_NO_DEFAULT_ALPN, + SVCB_KEY_PORT, + SVCB_KEY_IPV4HINT, + SVCB_KEY_ECH, + SVCB_KEY_IPV6HINT +} SVCBKey; + +static char * +svcb_key_to_string (SVCBKey key) +{ + switch (key) + { + case SVCB_KEY_MANDATORY: + return g_strdup ("mandatory"); + case SVCB_KEY_ALPN: + return g_strdup ("alpn"); + case SVCB_KEY_NO_DEFAULT_ALPN: + return g_strdup ("no-default-alpn"); + case SVCB_KEY_PORT: + return g_strdup ("port"); + case SVCB_KEY_IPV4HINT: + return g_strdup ("ipv4hint"); + case SVCB_KEY_ECH: + return g_strdup ("ech"); + case SVCB_KEY_IPV6HINT: + return g_strdup ("ipv6hint"); + default: + return g_strdup_printf ("key%u", key); + } +} + +static gchar * +get_uncompressed_domain (const guint8 *src, + const guint8 *end, + const guint8 **out) +{ + /* Length is same as input (all length octets replaced with '.') plus trailing NUL */ + GString *target = g_string_sized_new (end - src + 1); + guint n_labels = 0; + +#define MAX_LABEL_LENGTH 63 +#define MAX_LABEL_NUMBER 255 + + /* Defined in RFC 1035 section 3.1 */ + do + { + guint8 length = *(src++); + + if (length > (gsize) (end - src) + || length > MAX_LABEL_LENGTH + || n_labels == MAX_LABEL_NUMBER) + { + g_debug ("Received invalid length compressed domain."); + return g_string_free (target, TRUE); + } + + /* End of string */ + if (length == 0) + { + if (target->len == 0) + g_string_append_c (target, '.'); + break; + } + + g_string_append_len (target, (char *)src, length); + g_string_append_c (target, '.'); + src += length; + n_labels++; + } + while (src < end); + + *out = src; + return g_string_free (target, FALSE); +} + +static GVariant * +get_svcb_ipv4_hint_value (const guint8 *src, + guint16 length) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY); + + for (; length >= 4; length -= 4, src += 4) + { + char buffer[INET_ADDRSTRLEN]; + + if (inet_ntop (AF_INET, src, buffer, sizeof (buffer))) + g_variant_builder_add_value (&builder, g_variant_new_string (buffer)); + } + + if (length != 0) + { + g_debug ("Received invalid length ipv4hint."); + g_variant_builder_clear (&builder); + return NULL; + } + + return g_variant_builder_end (&builder); +} + +static GVariant * +get_svcb_ipv6_hint_value (const guint8 *src, + guint16 length) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY); + +#ifdef HAVE_IPV6 + for (; length >= 16; length -= 16, src += 16) + { + char buffer[INET6_ADDRSTRLEN]; + + if (inet_ntop (AF_INET6, src, buffer, sizeof (buffer))) + g_variant_builder_add_value (&builder, g_variant_new_string (buffer)); + } + + if (length != 0) + { + g_debug ("Received invalid length ipv6hint."); + g_variant_builder_clear (&builder); + return NULL; + } + +#endif + + return g_variant_builder_end (&builder); +} + +static GVariant * +get_svcb_alpn_value (const guint8 *src, + guint16 length) +{ + GVariantBuilder builder; + GString *alpn_id = NULL; + guchar alpn_id_remaining = 0; + + /* Format defined in Section 6.1 */ + g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY); + + /* length prefixed strings */ + for (; length > 0; src++, length--) + { + guchar c = *src; + + if (alpn_id != NULL && alpn_id_remaining == 0) + { + g_variant_builder_add_value (&builder, + g_variant_new_take_string (g_string_free (g_steal_pointer (&alpn_id), FALSE))); + /* This drops through as the current char is the new length */ + } + + if (alpn_id == NULL) + { + /* First byte is length */ + alpn_id_remaining = c; + alpn_id = g_string_sized_new (((gsize)c) + 1); + continue; + } + + g_string_append_c (alpn_id, c); + alpn_id_remaining--; + } + + /* Trailing value */ + if (alpn_id != NULL) + { + g_variant_builder_add_value (&builder, + g_variant_new_take_string (g_string_free (g_steal_pointer (&alpn_id), FALSE))); + } + + return g_variant_builder_end (&builder); +} + +static GVariant * +get_svcb_mandatory_value (const guint8 *src, + guint16 length) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY); + + for (; length > 1; length -= 2) + { + guint16 key; + gchar *key_str; + + GETSHORT (key, src); + key_str = svcb_key_to_string (key); + + g_variant_builder_add_value (&builder, + g_variant_new_take_string (key_str)); + } + + if (length != 0) + { + g_debug ("Received invalid length mandatory."); + g_variant_builder_clear (&builder); + return NULL; + } + + return g_variant_builder_end (&builder); +} + +static gboolean +get_svcb_value (SVCBKey key, + guint16 value_length, + const guint8 *src, + GVariant **value) +{ + switch (key) + { + case SVCB_KEY_MANDATORY: + *value = get_svcb_mandatory_value (src, value_length); + return *value != NULL; + case SVCB_KEY_ALPN: + *value = get_svcb_alpn_value (src, value_length); + return TRUE; + case SVCB_KEY_PORT: + { + guint16 port; + GETSHORT (port, src); + /* 0-65535 is valid */ + *value = g_variant_new_uint16 (port); + return TRUE; + } + case SVCB_KEY_ECH: + { + guint8 length; + GETSHORT (length, src); + + if (length > (value_length - 1)) + return FALSE; + + *value = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, src, length, 1); + return TRUE; + } + case SVCB_KEY_IPV4HINT: + *value = get_svcb_ipv4_hint_value (src, value_length); + return *value != NULL; + case SVCB_KEY_IPV6HINT: + *value = get_svcb_ipv6_hint_value (src, value_length); + return *value != NULL; + case SVCB_KEY_NO_DEFAULT_ALPN: + *value = g_variant_new_string (""); + return TRUE; + default: + { + *value = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, src, value_length, 1); + return TRUE; + } + } +} + +static GVariant *parse_res_https (const guint8 *answer, + const guint8 *end, + const guint8 **p, + GError **error) +{ + GVariant *variant; + gchar *target; + guint16 priority; + GVariantBuilder params_builder; + + /* This response has two forms: + * if priority is 0 it is AliasForm and the string is + * the alias target with nothing else. + * otherwise it is ServiceForm which is an alternative endpoint + * and extra params are included. + * + * https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/ + */ + + GETSHORT (priority, *p); + target = get_uncompressed_domain (*p, end, p); + + if (target == NULL) + goto invalid_input; + + /* For AliasForm we just include an empty dict. */ + g_variant_builder_init (¶ms_builder, G_VARIANT_TYPE ("a{sv}")); + if (priority != 0) + { + while (*p < end) + { + gchar *key_str; + GVariant *value = NULL; + guint16 key; + guint16 value_length; + + GETSHORT (key, *p); + GETSHORT (value_length, *p); + + if (value_length > (gsize) (end - *p)) + goto invalid_input; + + key_str = svcb_key_to_string (key); + + if (!get_svcb_value (key, value_length, *p, &value)) + goto invalid_input; + + g_variant_builder_add (¶ms_builder, "{sv}", key_str, value); + + *p += value_length; + g_free (key_str); + } + } + + variant = g_variant_new ("(qsa{sv})", priority, target, ¶ms_builder); + g_free (target); + return variant; + +invalid_input: + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, "Invalid HTTPS response data"); + *p = end; + g_free (target); + return NULL; +} + static gint g_resolver_record_type_to_rrtype (GResolverRecordType type) { @@ -648,6 +976,8 @@ g_resolver_record_type_to_rrtype (GResolverRecordType type) return T_NS; case G_RESOLVER_RECORD_MX: return T_MX; + case G_RESOLVER_RECORD_HTTPS: + return T_HTTPS; } g_return_val_if_reached (-1); } @@ -667,6 +997,7 @@ g_resolver_records_from_res_query (const gchar *rrname, const HEADER *header; GList *records; GVariant *record; + GError *parsing_error = NULL; if (len <= 0) { @@ -739,6 +1070,9 @@ g_resolver_records_from_res_query (const gchar *rrname, case T_TXT: record = parse_res_txt (answer, p + rdlength, &p); break; + case T_HTTPS: + record = parse_res_https (answer, p + rdlength, &p, &parsing_error); + break; default: g_warn_if_reached (); record = NULL; @@ -747,9 +1081,18 @@ g_resolver_records_from_res_query (const gchar *rrname, if (record != NULL) records = g_list_prepend (records, record); + + if (parsing_error) + break; } - if (records == NULL) + if (parsing_error) + { + g_propagate_prefixed_error (error, parsing_error, _("Failed to parse DNS response for ā%sā: "), rrname); + g_list_free_full (records, (GDestroyNotify)g_variant_unref); + return NULL; + } + else if (!records) { g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND, _("No DNS record of the requested type for ā%sā"), rrname); @@ -762,6 +1105,10 @@ g_resolver_records_from_res_query (const gchar *rrname, #elif defined(G_OS_WIN32) +#ifndef DNS_TYPE_HTTPS +#define DNS_TYPE_HTTPS 65 +#endif + static GVariant * parse_dns_srv (DNS_RECORD *rec) { @@ -830,6 +1177,8 @@ g_resolver_record_type_to_dnstype (GResolverRecordType type) return DNS_TYPE_NS; case G_RESOLVER_RECORD_MX: return DNS_TYPE_MX; + case G_RESOLVER_RECORD_HTTPS: + return DNS_TYPE_HTTPS; } g_return_val_if_reached (-1); } diff --git a/gio/meson.build b/gio/meson.build index 5e879fd3d..af0f6bfdd 100644 --- a/gio/meson.build +++ b/gio/meson.build @@ -74,6 +74,14 @@ if host_system != 'windows' endif endif + # dn_comp() + if cc.links('''#include <resolv.h> + int main (int argc, char ** argv) { + return dn_comp(NULL, NULL, 0, NULL, NULL) == -1; + } ''', args : network_args, name : 'dn_comp()') + glib_conf.set('HAVE_DN_COMP', 1) + endif + # res_nclose() if cc.links('''#include <sys/types.h> #include <netinet/in.h> diff --git a/gio/tests/gresolver.c b/gio/tests/gresolver.c new file mode 100644 index 000000000..a81f01681 --- /dev/null +++ b/gio/tests/gresolver.c @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2021 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Patrick Griffis <pgriffis@igalia.com> + */ + +#include "config.h" + +#include <glib.h> +#include <gio/gnetworking.h> + +#define GIO_COMPILATION +#include "gthreadedresolver.h" +#undef GIO_COMPILATION + +#ifdef HAVE_DN_COMP +static void +dns_builder_add_uint8 (GByteArray *builder, + guint8 value) +{ + g_byte_array_append (builder, &value, 1); +} + +static void +dns_builder_add_uint16 (GByteArray *builder, + guint16 value) +{ + dns_builder_add_uint8 (builder, (value >> 8) & 0xFF); + dns_builder_add_uint8 (builder, (value) & 0xFF); +} + +static void +dns_builder_add_uint32 (GByteArray *builder, + guint32 value) +{ + dns_builder_add_uint8 (builder, (value >> 24) & 0xFF); + dns_builder_add_uint8 (builder, (value >> 16) & 0xFF); + dns_builder_add_uint8 (builder, (value >> 8) & 0xFF); + dns_builder_add_uint8 (builder, (value) & 0xFF); +} + +static void +dns_builder_add_length_prefixed_string (GByteArray *builder, + const char *string) +{ + guint8 length; + + g_assert (strlen (string) <= G_MAXUINT8); + + length = (guint8) strlen (string); + dns_builder_add_uint8 (builder, length); + + /* Don't include trailing NUL */ + g_byte_array_append (builder, (const guchar *)string, length); +} + +static void +dns_builder_add_domain (GByteArray *builder, + const char *string) +{ + int ret; + guchar buffer[256]; + + ret = dn_comp (string, buffer, sizeof (buffer), NULL, NULL); + g_assert (ret != -1); + + g_byte_array_append (builder, buffer, ret); +} + +static void +dns_builder_add_answer_data (GByteArray *builder, + GByteArray *answer) +{ + dns_builder_add_uint16 (builder, answer->len); /* rdlength */ + g_byte_array_append (builder, answer->data, answer->len); +} +#endif /* HAVE_DN_COMP */ + +typedef struct +{ + GByteArray *answer; +} TestData; + +static void +dns_test_setup (TestData *fixture, + gconstpointer user_data) +{ + fixture->answer = g_byte_array_sized_new (2046); + +#ifdef HAVE_DN_COMP + /* Start with a header, we ignore everything except ancount. + https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1 */ + dns_builder_add_uint16 (fixture->answer, 0); /* ID */ + dns_builder_add_uint16 (fixture->answer, 0); /* |QR| Opcode |AA|TC|RD|RA| Z | RCODE | */ + dns_builder_add_uint16 (fixture->answer, 0); /* QDCOUNT */ + dns_builder_add_uint16 (fixture->answer, 1); /* ANCOUNT (1 answer) */ + dns_builder_add_uint16 (fixture->answer, 0); /* NSCOUNT */ + dns_builder_add_uint16 (fixture->answer, 0); /* ARCOUNT */ + + /* Answer section */ + dns_builder_add_domain (fixture->answer, "example.org"); + dns_builder_add_uint16 (fixture->answer, 65); /* type=HTTPS */ + dns_builder_add_uint16 (fixture->answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (fixture->answer, 0); /* ttl (ignored) */ + /* Next one will be rdlength which is test specific. */ +#endif +} + +static void +dns_test_teardown (TestData *fixture, + gconstpointer user_data) +{ + g_byte_array_free (fixture->answer, TRUE); +} + +static void +test_https_alias (TestData *fixture, + gconstpointer user_data) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GList *records; + GByteArray *https_answer = g_byte_array_sized_new (1024); + guint16 priority; + const char *alias; + GError *error = NULL; + + dns_builder_add_uint16 (https_answer, 0); /* priority */ + dns_builder_add_length_prefixed_string (https_answer, "foo.example.org"); /* alias target */ + + dns_builder_add_answer_data (fixture->answer, https_answer); + records = g_resolver_records_from_res_query ("example.org", 65, + (guchar*)fixture->answer->data, + fixture->answer->len, + 0, &error); + + g_assert_no_error (error); + g_assert_cmpuint (g_list_length (records), ==, 1); + g_variant_get (records->data, "(q&sa{sv})", &priority, &alias, NULL); + + g_assert_cmpuint (priority, ==, 0); + g_assert_cmpstr (alias, ==, "foo.example.org."); + + g_list_free_full (records, (GDestroyNotify)g_variant_unref); + g_byte_array_free (https_answer, TRUE); +#endif /* HAVE_DN_COMP */ +} + +static void +test_https_service (TestData *fixture, + gconstpointer user_data) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GList *records; + GByteArray *https_answer = g_byte_array_sized_new (1024); + guint16 priority; + const char *target; + GVariant *params; + guint16 port; + GVariantDict *dict; + GError *error = NULL; + const char **alpn, **mandatory; + GVariant *key123_variant; + const char *key123; + gsize key123_size; + + dns_builder_add_uint16 (https_answer, 1); /* priority */ + dns_builder_add_length_prefixed_string (https_answer, ""); /* target */ + + dns_builder_add_uint16 (https_answer, 3); /* SVCB key "port" */ + dns_builder_add_uint16 (https_answer, 2); /* Value length */ + dns_builder_add_uint16 (https_answer, 4443); /* SVCB value */ + + dns_builder_add_uint16 (https_answer, 0); /* SVCB key "mandatory" */ + dns_builder_add_uint16 (https_answer, 2); /* Value length */ + dns_builder_add_uint16 (https_answer, 3); /* SVCB value */ + + dns_builder_add_uint16 (https_answer, 1); /* SVCB key "alpn" */ + dns_builder_add_uint16 (https_answer, 3); /* Value length */ + dns_builder_add_length_prefixed_string (https_answer, "h2"); + + dns_builder_add_uint16 (https_answer, 123); /* SVCB key "key123" */ + dns_builder_add_uint16 (https_answer, 4); /* Value length */ + dns_builder_add_length_prefixed_string (https_answer, "idk"); + + dns_builder_add_answer_data (fixture->answer, https_answer); + records = g_resolver_records_from_res_query ("example.org", 65, + (guchar *)fixture->answer->data, + fixture->answer->len, + 0, &error); + + g_assert_no_error (error); + g_assert_cmpuint (g_list_length (records), ==, 1); + g_variant_get (records->data, "(q&s@a{sv})", &priority, &target, ¶ms); + + g_assert_cmpuint (priority, ==, 1); + g_assert_cmpstr (target, ==, "."); + g_assert_true (g_variant_is_of_type (params, G_VARIANT_TYPE_VARDICT)); + dict = g_variant_dict_new (params); + g_assert_true (g_variant_dict_lookup (dict, "port", "q", &port)); + g_assert_cmpuint (port, ==, 4443); + g_assert_true (g_variant_dict_lookup (dict, "alpn", "^a&s", &alpn)); + g_assert_cmpstr (alpn[0], ==, "h2"); + g_assert_true (g_variant_dict_lookup (dict, "mandatory", "^a&s", &mandatory)); + g_assert_cmpstr (mandatory[0], ==, "port"); + g_assert_true (g_variant_dict_lookup (dict, "key123", "@ay", &key123_variant)); + key123 = g_variant_get_fixed_array (key123_variant, &key123_size, 1); + g_assert_true (!strncmp (key123 + 1, "idk", key123_size - 1)); + + g_variant_dict_unref (dict); + g_variant_unref (params); + g_list_free_full (records, (GDestroyNotify)g_variant_unref); + g_byte_array_free (https_answer, TRUE); +#endif +} + +static void +test_https_invalid_1 (TestData *fixture, + gconstpointer user_data) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GList *records; + GByteArray *https_answer = g_byte_array_sized_new (1024); + GError *error = NULL; + + dns_builder_add_uint16 (https_answer, 1); /* priority */ + dns_builder_add_length_prefixed_string (https_answer, ""); /* target */ + + /* Invalid value length is too long and will be caught. */ + dns_builder_add_uint16 (https_answer, 3); /* SVCB key "port" */ + dns_builder_add_uint16 (https_answer, 100); /* Value length */ + dns_builder_add_uint16 (https_answer, 4443); /* SVCB value */ + + dns_builder_add_answer_data (fixture->answer, https_answer); + records = g_resolver_records_from_res_query ("example.org", 65, + (guchar *)fixture->answer->data, + fixture->answer->len, + 0, &error); + + g_assert_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL); + g_assert_null (records); + + g_error_free (error); + g_byte_array_free (https_answer, TRUE); +#endif +} + +static void +test_https_invalid_2 (TestData *fixture, + gconstpointer user_data) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GList *records; + GByteArray *https_answer = g_byte_array_sized_new (1024); + GError *error = NULL; + + dns_builder_add_uint16 (https_answer, 1); /* priority */ + dns_builder_add_length_prefixed_string (https_answer, ""); /* target */ + + /* Within a SVCB value, having invalid length will also be caught. */ + dns_builder_add_uint16 (https_answer, 5); /* SVCB key "ECH" */ + dns_builder_add_uint16 (https_answer, 2); /* Value length */ + dns_builder_add_uint16 (https_answer, 1000); /* SVCB value (prefixed string, invalid length) */ + + dns_builder_add_answer_data (fixture->answer, https_answer); + records = g_resolver_records_from_res_query ("example.org", 65, + (guchar *)fixture->answer->data, + fixture->answer->len, + 0, &error); + + g_assert_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL); + g_assert_null (records); + + g_error_free (error); + g_byte_array_free (https_answer, TRUE); +#endif +} + +static void +test_https_invalid_3 (TestData *fixture, + gconstpointer user_data) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GList *records; + GByteArray *https_answer = g_byte_array_sized_new (1024); + GError *error = NULL; + + dns_builder_add_uint16 (https_answer, 0); /* priority */ + + /* Creating an invalid target string will be caught. */ + dns_builder_add_uint8 (https_answer, 100); + g_byte_array_append (https_answer, (const guchar *)"test", 4); + + dns_builder_add_answer_data (fixture->answer, https_answer); + records = g_resolver_records_from_res_query ("example.org", 65, + (guchar *)fixture->answer->data, + fixture->answer->len, + 0, &error); + + g_assert_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL); + g_assert_null (records); + + g_error_free (error); + g_byte_array_free (https_answer, TRUE); +#endif +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add ("/gresolver/https/alias", TestData, NULL, dns_test_setup, test_https_alias, dns_test_teardown); + g_test_add ("/gresolver/https/service", TestData, NULL, dns_test_setup, test_https_service, dns_test_teardown); + g_test_add ("/gresolver/https/invalid/1", TestData, NULL, dns_test_setup, test_https_invalid_1, dns_test_teardown); + g_test_add ("/gresolver/https/invalid/2", TestData, NULL, dns_test_setup, test_https_invalid_2, dns_test_teardown); + g_test_add ("/gresolver/https/invalid/3", TestData, NULL, dns_test_setup, test_https_invalid_3, dns_test_teardown); + + return g_test_run (); +} diff --git a/gio/tests/meson.build b/gio/tests/meson.build index f8cf7b655..88607510c 100644 --- a/gio/tests/meson.build +++ b/gio/tests/meson.build @@ -67,6 +67,7 @@ gio_tests = { 'g-icon' : {}, 'gdbus-addresses' : {}, 'gdbus-message' : {}, + 'gresolver' : {'dependencies' : [network_libs]}, 'inet-address' : {}, 'io-stream' : {}, 'memory-input-stream' : {}, diff --git a/gio/tests/resolver.c b/gio/tests/resolver.c index 6e0c4d73b..2052c9c32 100644 --- a/gio/tests/resolver.c +++ b/gio/tests/resolver.c @@ -308,6 +308,52 @@ print_resolved_ns (const char *rrname, } static void +print_resolved_https (const char *rrname, + GList *records, + GError *error) +{ + GList *t; + + G_LOCK (response); + printf ("Zone: %s\n", rrname); + if (error) + { + printf ("Error: %s\n", error->message); + g_error_free (error); + } + else if (!records) + { + printf ("no HTTPS records\n"); + } + else + { + for (t = records; t; t = t->next) + { + guint16 priority; + gchar *target, *params_str; + GVariant *params; + + g_variant_get (t->data, "(qs@a{sv})", &priority, &target, ¶ms); + + printf ("Priority: %u\nTarget: %s\n", priority, target); + + params_str = g_variant_print (params, FALSE); + printf ("Params: %s\n", params_str); + + g_free (params_str); + g_free (target); + g_variant_unref (params); + g_variant_unref (t->data); + } + g_list_free (records); + } + printf ("\n"); + + done_lookup (); + G_UNLOCK (response); +} + +static void lookup_one_sync (const char *arg) { GError *error = NULL; @@ -331,6 +377,9 @@ lookup_one_sync (const char *arg) case G_RESOLVER_RECORD_TXT: print_resolved_txt (arg, records, error); break; + case G_RESOLVER_RECORD_HTTPS: + print_resolved_https (arg, records, error); + break; default: g_warn_if_reached (); break; @@ -449,6 +498,9 @@ lookup_records_callback (GObject *source, case G_RESOLVER_RECORD_TXT: print_resolved_txt (arg, records, error); break; + case G_RESOLVER_RECORD_HTTPS: + print_resolved_https (arg, records, error); + break; default: g_warn_if_reached (); break; @@ -659,9 +711,11 @@ record_type_arg (const gchar *option_name, record_type = G_RESOLVER_RECORD_SOA; } else if (g_ascii_strcasecmp (value, "NS") == 0) { record_type = G_RESOLVER_RECORD_NS; + } else if (g_ascii_strcasecmp (value, "HTTPS") == 0) { + record_type = G_RESOLVER_RECORD_HTTPS; } else { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, - "Specify MX, TXT, NS or SOA for the special record lookup types"); + "Specify MX, TXT, NS, SOA, or HTTPS for the special record lookup types"); return FALSE; } @@ -671,7 +725,7 @@ record_type_arg (const gchar *option_name, static const GOptionEntry option_entries[] = { { "synchronous", 's', 0, G_OPTION_ARG_NONE, &synchronous, "Synchronous connections", NULL }, { "connectable", 'c', 0, G_OPTION_ARG_INT, &connectable_count, "Connectable count", "C" }, - { "special-type", 't', 0, G_OPTION_ARG_CALLBACK, record_type_arg, "Record type like MX, TXT, NS or SOA", "RR" }, + { "special-type", 't', 0, G_OPTION_ARG_CALLBACK, record_type_arg, "Record type like MX, TXT, NS, SOA, or HTTPS", "RR" }, G_OPTION_ENTRY_NULL, }; |