[glib/pgriffis/wip/resolver-https] gresolver: Add support for the HTTPS DNS type




commit 32d76b44fb44a5ee79e9c23ab8abebd5a2df273d
Author: Patrick Griffis <pgriffis igalia com>
Date:   Mon Apr 19 12:17:58 2021 -0500

    gresolver: Add support for the HTTPS DNS type
    
    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.

 gio/gioenums.h          |  17 ++-
 gio/gthreadedresolver.c | 309 ++++++++++++++++++++++++++++++++++++++++++++-
 gio/gthreadedresolver.h |   9 ++
 gio/meson.build         |   8 ++
 gio/tests/gresolver.c   | 325 ++++++++++++++++++++++++++++++++++++++++++++++++
 gio/tests/meson.build   |   1 +
 gio/tests/resolver.c    |  58 ++++++++-
 7 files changed, 722 insertions(+), 5 deletions(-)
---
diff --git a/gio/gioenums.h b/gio/gioenums.h
index d81ada416..4dc2df456 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
+ *   `port`: uint16
+ *   `ipv4hint`: array of strings of addresses
+ *   `ipv6hint`: array of strings of addresses
+ *   `ech`: string of base64 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 key number, the value is a bytestring 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 93794b5b3..acc10f0a4 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,290 @@ parse_res_txt (guchar  *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 (guchar *src, guchar *end, guchar **out)
+{
+  GString *target = g_string_new (NULL);
+
+  /* Defined in RFC 1035 section 3.1 */
+  do
+    {
+      guint8 length = *(src++);
+
+      if (length > (gsize) (end - src))
+        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;
+    }
+  while (src < end);
+
+  *out = src;
+  return g_string_free (target, FALSE);
+}
+
+static GVariant *
+get_svcb_ipv4_hint_value (const guchar *src, const guchar *end, 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));
+    }
+
+  return g_variant_builder_end (&builder);
+}
+
+static GVariant *
+get_svcb_ipv6_hint_value (const guchar *src, const guchar *end, 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));
+    }
+#endif
+
+  return g_variant_builder_end (&builder);
+}
+
+static GVariant *
+get_svcb_alpn_value (const guchar *src, const guchar *end, guint16 length)
+{
+  GVariantBuilder builder;
+  GString *alpn_id = NULL;
+  guchar alpn_id_remaning = 0;
+
+  /* Format defined in Section 6.1 */
+  g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY);
+
+  /* length prefixed strings */
+  for (; src <= end && length; src++, length--)
+    {
+      guchar c = *src;
+
+      if (alpn_id && !alpn_id_remaning)
+        {
+          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)
+        {
+          /* First byte is length */
+          alpn_id_remaning = c;
+          alpn_id = g_string_sized_new (c + 1);
+          continue;
+        }
+
+      g_string_append_c (alpn_id, c);
+      alpn_id_remaning--;
+    }
+
+  /* Trailing value */
+  if (alpn_id)
+    {
+      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 guchar *src, const guchar *end, guint16 length)
+{
+  GVariantBuilder builder;
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY);
+
+  for (; length; 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));
+    }
+
+  return g_variant_builder_end (&builder);
+}
+
+static gboolean
+get_svcb_value (SVCBKey key, guint16 value_length, const guchar *src, const guchar *end, GVariant **value)
+{
+  switch (key)
+    {
+    case SVCB_KEY_MANDATORY:
+      *value = get_svcb_mandatory_value (src, end, value_length);
+      return TRUE;
+    case SVCB_KEY_ALPN:
+      *value = get_svcb_alpn_value (src, end, value_length);
+      return TRUE;
+    case SVCB_KEY_PORT:
+      {
+        guint16 port;
+        GETSHORT (port, src);
+        *value = g_variant_new_uint16 (port);
+        return TRUE;
+      }
+    case SVCB_KEY_ECH:
+      {
+        guint8 length;
+        gchar *base64_str;
+        GETSHORT (length, src);
+
+        if (length > (gsize) (end - src))
+          return FALSE;
+
+        base64_str = g_base64_encode (src, length);
+        *value = g_variant_new_take_string (base64_str);
+        return TRUE;
+      }
+    case SVCB_KEY_IPV4HINT:
+      *value = get_svcb_ipv4_hint_value (src, end, value_length);
+      return TRUE;
+    case SVCB_KEY_IPV6HINT:
+      *value = get_svcb_ipv6_hint_value (src, end, value_length);
+      return TRUE;
+    case SVCB_KEY_NO_DEFAULT_ALPN:
+      G_GNUC_FALLTHROUGH;
+    default:
+      {
+        gchar *string = g_strndup ((char *)src, value_length);
+        *value = g_variant_new_bytestring (string);
+        g_free (string);
+        return TRUE;
+      }
+    }
+}
+
+static GVariant *parse_res_https (guchar  *answer,
+                 guchar  *end,
+                 guchar **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 (&params_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, end, &value))
+            goto invalid_input;
+
+          g_variant_builder_add (&params_builder, "{sv}", key_str, value);
+
+          *p += value_length;
+          g_free (key_str);
+        }
+    }
+
+  variant = g_variant_new ("(qsa{sv})", priority, target, &params_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,11 +936,13 @@ 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);
 }
 
-static GList *
+GList *
 g_resolver_records_from_res_query (const gchar      *rrname,
                                    gint              rrtype,
                                    guchar           *answer,
@@ -667,6 +957,7 @@ g_resolver_records_from_res_query (const gchar      *rrname,
   HEADER *header;
   GList *records;
   GVariant *record;
+  GError *parsing_error = NULL;
 
   if (len <= 0)
     {
@@ -739,6 +1030,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;
@@ -749,7 +1043,12 @@ g_resolver_records_from_res_query (const gchar      *rrname,
         records = g_list_prepend (records, record);
     }
 
-  if (records == NULL)
+  if (parsing_error)
+    {
+      g_propagate_prefixed_error (error, parsing_error, _("Failed to parse DNS response for ā€œ%sā€: "), 
rrname);
+      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 +1061,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 +1133,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/gthreadedresolver.h b/gio/gthreadedresolver.h
index 5900d6a14..9b0f98222 100644
--- a/gio/gthreadedresolver.h
+++ b/gio/gthreadedresolver.h
@@ -42,6 +42,15 @@ typedef struct {
 GLIB_AVAILABLE_IN_ALL
 GType g_threaded_resolver_get_type (void) G_GNUC_CONST;
 
+/* Used for a private test API */
+GLIB_AVAILABLE_IN_ALL
+GList *g_resolver_records_from_res_query (const gchar      *rrname,
+                                          gint              rrtype,
+                                          guchar           *answer,
+                                          gint              len,
+                                          gint              herr,
+                                          GError          **error);
+
 G_END_DECLS
 
 #endif /* __G_RESOLVER_H__ */
diff --git a/gio/meson.build b/gio/meson.build
index ac3373f2b..028076dd8 100644
--- a/gio/meson.build
+++ b/gio/meson.build
@@ -85,6 +85,14 @@ if host_system != 'windows'
     glib_conf.set('HAVE_RES_INIT', 1)
   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..b9ffab6ba
--- /dev/null
+++ b/gio/tests/gresolver.c
@@ -0,0 +1,325 @@
+/*
+ * 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_uint16 (GString *builder, guint16 value)
+{
+    g_string_append_c (builder, (value & 0xFF00) >> 8);
+    g_string_append_c (builder, (value & 0xFF));
+}
+
+static void
+dns_builder_add_uint32 (GString *builder, guint32 value)
+{
+    g_string_append_c (builder, (value & 0xFF000000) >> 24);
+    g_string_append_c (builder, (value & 0xFF0000) >> 16);
+    g_string_append_c (builder, (value & 0xFF00) >> 16);
+    g_string_append_c (builder, (value & 0xFF) >> 16);
+}
+
+static void
+dns_builder_add_length_prefixed_string (GString *builder, const char *string)
+{
+    guint8 length = (guint8) strlen (string);
+    g_string_append_c (builder, length);
+
+    /* Don't include trailing NUL */
+    g_string_append_len (builder, string, length);
+}
+
+static void
+dns_builder_add_domain (GString *builder, const char *string)
+{
+  int ret;
+  guchar buffer[256];
+
+  ret = dn_comp (string, buffer, sizeof (buffer), NULL, NULL);
+  g_assert (ret != -1);
+
+  g_string_append_len (builder, buffer, ret);
+}
+
+static void
+dns_builder_add_answer_data (GString *builder, GString *answer)
+{
+  dns_builder_add_uint16 (builder, answer->len); /* rdlength */
+  g_string_append_len (builder, answer->str, answer->len);
+}
+#endif /* HAVE_DN_COMP */
+
+typedef struct
+{
+  GString *answer;
+} TestData;
+
+static void
+dns_test_setup (TestData      *fixture,
+                gconstpointer  user_data)
+{
+  fixture->answer = g_string_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_string_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;
+  GString *https_answer = g_string_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->str,
+                                               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_string_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;
+  GString *https_answer = g_string_sized_new (1024);
+  guint16 priority;
+  const char *target;
+  GVariant *params;
+  guint16 port;
+  GVariantDict *dict;
+  GError *error = NULL;
+  const char **alpn, **mandatory;
+  const char *key123;
+
+  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->str,
+                                               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, &params);
+
+  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));
+  g_assert_cmpstr (key123 + 1, ==, "idk");
+
+  g_variant_dict_unref (dict);
+  g_variant_unref (params);
+  g_list_free_full (records, (GDestroyNotify)g_variant_unref);
+  g_string_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;
+  GString *https_answer = g_string_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->str,
+                                               fixture->answer->len,
+                                               0, &error);
+
+  g_assert_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL);
+  g_assert_null (records);
+
+  g_string_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;
+  GString *https_answer = g_string_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->str,
+                                               fixture->answer->len,
+                                               0, &error);
+
+  g_assert_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL);
+  g_assert_null (records);
+
+  g_string_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;
+  GString *https_answer = g_string_sized_new (1024);
+  GError *error = NULL;
+
+  dns_builder_add_uint16 (https_answer, 0); /* priority */
+
+  /* Creating an invalid target string will be caught. */
+  g_string_append_c (https_answer, 100);
+  g_string_append_len (https_answer, "test", 4);
+
+  dns_builder_add_answer_data (fixture->answer, https_answer);
+  records = g_resolver_records_from_res_query ("example.org", 65,
+                                               (guchar*)fixture->answer->str,
+                                               fixture->answer->len,
+                                               0, &error);
+
+  g_assert_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL);
+  g_assert_null (records);
+
+  g_string_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 5dbfb8e60..63a5ab0af 100644
--- a/gio/tests/meson.build
+++ b/gio/tests/meson.build
@@ -64,6 +64,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
@@ -307,6 +307,52 @@ print_resolved_ns (const char *rrname,
   G_UNLOCK (response);
 }
 
+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, &params);
+
+          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)
 {
@@ -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,
 };
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]