diff --git a/Makefile-libostree-defines.am b/Makefile-libostree-defines.am index 7586f7b9..6fc4a18a 100644 --- a/Makefile-libostree-defines.am +++ b/Makefile-libostree-defines.am @@ -43,6 +43,7 @@ libostree_public_headers += \ src/libostree/ostree-ref.h \ src/libostree/ostree-remote.h \ src/libostree/ostree-repo-finder.h \ + src/libostree/ostree-repo-finder-avahi.h \ src/libostree/ostree-repo-finder-config.h \ src/libostree/ostree-repo-finder-mount.h \ $(NULL) diff --git a/Makefile-libostree.am b/Makefile-libostree.am index 6a7c4820..8ff89e52 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -155,6 +155,7 @@ libostree_1_la_SOURCES += \ src/libostree/ostree-ref.h \ src/libostree/ostree-remote.h \ src/libostree/ostree-repo-finder.h \ + src/libostree/ostree-repo-finder-avahi.h \ src/libostree/ostree-repo-finder-config.h \ src/libostree/ostree-repo-finder-mount.h \ $(NULL) @@ -163,9 +164,17 @@ libostree_1_la_SOURCES += \ src/libostree/ostree-bloom.c \ src/libostree/ostree-bloom-private.h \ src/libostree/ostree-repo-finder.c \ + src/libostree/ostree-repo-finder-avahi.c \ src/libostree/ostree-repo-finder-config.c \ src/libostree/ostree-repo-finder-mount.c \ $(NULL) + +if USE_AVAHI +libostree_1_la_SOURCES += \ + src/libostree/ostree-repo-finder-avahi-parser.c \ + src/libostree/ostree-repo-finder-avahi-private.h \ + $(NULL) +endif # USE_AVAHI endif symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym @@ -193,6 +202,13 @@ libostree_1_la_CFLAGS += $(OT_DEP_LIBARCHIVE_CFLAGS) libostree_1_la_LIBADD += $(OT_DEP_LIBARCHIVE_LIBS) endif +if ENABLE_EXPERIMENTAL_API +if USE_AVAHI +libostree_1_la_CFLAGS += $(OT_DEP_AVAHI_CFLAGS) +libostree_1_la_LIBADD += $(OT_DEP_AVAHI_LIBS) +endif +endif + if BUILDOPT_LIBSYSTEMD libostree_1_la_CFLAGS += $(LIBSYSTEMD_CFLAGS) libostree_1_la_LIBADD += $(LIBSYSTEMD_LIBS) @@ -241,7 +257,7 @@ OSTree_1_0_gir_INCLUDES = Gio-2.0 OSTree_1_0_gir_CFLAGS = $(libostree_1_la_CFLAGS) OSTree_1_0_gir_LIBS = libostree-1.la OSTree_1_0_gir_SCANNERFLAGS = --warn-all --identifier-prefix=Ostree --symbol-prefix=ostree -OSTree_1_0_gir_FILES = $(libostreeinclude_HEADERS) $(filter-out %-private.h %/ostree-soup-uri.h %/ostree-repo-finder.h %/ostree-repo-finder-config.h %/ostree-repo-finder-mount.h,$(libostree_1_la_SOURCES)) +OSTree_1_0_gir_FILES = $(libostreeinclude_HEADERS) $(filter-out %-private.h %/ostree-soup-uri.h %/ostree-repo-finder.h %/ostree-repo-finder-avahi.h %/ostree-repo-finder-config.h %/ostree-repo-finder-mount.h,$(libostree_1_la_SOURCES)) INTROSPECTION_GIRS += OSTree-1.0.gir gir_DATA += OSTree-1.0.gir typelib_DATA += OSTree-1.0.typelib diff --git a/Makefile-tests.am b/Makefile-tests.am index d04a1cbc..c21e29f7 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -199,6 +199,10 @@ test_programs += \ tests/test-repo-finder-config \ tests/test-repo-finder-mount \ $(NULL) + +if USE_AVAHI +test_programs += tests/test-repo-finder-avahi +endif endif # An interactive tool @@ -231,6 +235,12 @@ tests_test_bloom_SOURCES = src/libostree/ostree-bloom.c tests/test-bloom.c tests_test_bloom_CFLAGS = $(TESTS_CFLAGS) tests_test_bloom_LDADD = $(TESTS_LDADD) +if USE_AVAHI +tests_test_repo_finder_avahi_SOURCES = src/libostree/ostree-repo-finder-avahi-parser.c tests/test-repo-finder-avahi.c +tests_test_repo_finder_avahi_CFLAGS = $(TESTS_CFLAGS) +tests_test_repo_finder_avahi_LDADD = $(TESTS_LDADD) +endif + tests_test_repo_finder_config_SOURCES = tests/test-repo-finder-config.c tests_test_repo_finder_config_CFLAGS = $(TESTS_CFLAGS) tests_test_repo_finder_config_LDADD = $(TESTS_LDADD) diff --git a/apidoc/ostree-experimental-sections.txt b/apidoc/ostree-experimental-sections.txt index c977b56c..4c71fad2 100644 --- a/apidoc/ostree-experimental-sections.txt +++ b/apidoc/ostree-experimental-sections.txt @@ -49,6 +49,16 @@ ostree_repo_finder_get_type ostree_repo_finder_result_get_type +
+ostree-repo-finder-avahi +OstreeRepoFinderAvahi +ostree_repo_finder_avahi_new +ostree_repo_finder_avahi_start +ostree_repo_finder_avahi_stop + +ostree_repo_finder_avahi_get_type +
+
ostree-repo-finder-config OstreeRepoFinderConfig diff --git a/configure.ac b/configure.ac index 9973c80a..67e70b3e 100644 --- a/configure.ac +++ b/configure.ac @@ -320,6 +320,31 @@ if test x$with_openssl != xno; then OSTREE_FEATURES="$OSTREE_FEATURES openssl"; AM_CONDITIONAL(USE_OPENSSL, test $with_openssl != no) dnl end openssl +dnl Avahi dependency for finding repos +AVAHI_DEPENDENCY="avahi-client >= 0.6.31 avahi-glib >= 0.6.31" + +AC_ARG_WITH(avahi, + AS_HELP_STRING([--without-avahi], [Do not use Avahi]), + :, with_avahi=maybe) + +AS_IF([ test x$with_avahi != xno ], [ + AC_MSG_CHECKING([for $AVAHI_DEPENDENCY]) + PKG_CHECK_EXISTS($AVAHI_DEPENDENCY, have_avahi=yes, have_avahi=no) + AC_MSG_RESULT([$have_avahi]) + AS_IF([ test x$have_avahi = xno && test x$with_avahi != xmaybe ], [ + AC_MSG_ERROR([Avahi is enabled but could not be found]) + ]) + AS_IF([ test x$have_avahi = xyes], [ + AC_DEFINE([HAVE_AVAHI], 1, [Define if we have avahi-client.pc and avahi-glib.pc]) + PKG_CHECK_MODULES(OT_DEP_AVAHI, $AVAHI_DEPENDENCY) + with_avahi=yes + ], [ + with_avahi=no + ]) +], [ with_avahi=no ]) +if test x$with_avahi != xno; then OSTREE_FEATURES="$OSTREE_FEATURES avahi"; fi +AM_CONDITIONAL(USE_AVAHI, test $with_avahi != no) + dnl This is what is in RHEL7.2 right now, picking it arbitrarily LIBMOUNT_DEPENDENCY="mount >= 2.23.0" diff --git a/src/libostree/libostree-experimental.sym b/src/libostree/libostree-experimental.sym index e70c80bb..32ba0929 100644 --- a/src/libostree/libostree-experimental.sym +++ b/src/libostree/libostree-experimental.sym @@ -47,6 +47,10 @@ global: ostree_collection_ref_new; ostree_repo_find_remotes_async; ostree_repo_find_remotes_finish; + ostree_repo_finder_avahi_get_type; + ostree_repo_finder_avahi_new; + ostree_repo_finder_avahi_start; + ostree_repo_finder_avahi_stop; ostree_repo_finder_config_get_type; ostree_repo_finder_config_new; ostree_repo_finder_get_type; diff --git a/src/libostree/ostree-autocleanups.h b/src/libostree/ostree-autocleanups.h index dd3c9778..b375413c 100644 --- a/src/libostree/ostree-autocleanups.h +++ b/src/libostree/ostree-autocleanups.h @@ -64,6 +64,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeCollectionRef, ostree_collection_ref_free) G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeCollectionRefv, ostree_collection_ref_freev, NULL) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRemote, ostree_remote_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinder, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderAvahi, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderConfig, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderMount, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderResult, ostree_repo_finder_result_free) diff --git a/src/libostree/ostree-core-private.h b/src/libostree/ostree-core-private.h index a56fdc0b..76c76cc7 100644 --- a/src/libostree/ostree-core-private.h +++ b/src/libostree/ostree-core-private.h @@ -188,6 +188,9 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinder, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderResult, ostree_repo_finder_result_free) G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeRepoFinderResultv, ostree_repo_finder_result_freev, NULL) +#include "ostree-repo-finder-avahi.h" +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderAvahi, g_object_unref) + #include "ostree-repo-finder-config.h" G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderConfig, g_object_unref) diff --git a/src/libostree/ostree-repo-finder-avahi-parser.c b/src/libostree/ostree-repo-finder-avahi-parser.c new file mode 100644 index 00000000..805f5dff --- /dev/null +++ b/src/libostree/ostree-repo-finder-avahi-parser.c @@ -0,0 +1,137 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2016 Kinvolk GmbH + * Copyright © 2017 Endless Mobile, Inc. + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: + * - Krzesimir Nowak + * - Philip Withnall + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "ostree-autocleanups.h" +#include "ostree-repo-finder-avahi.h" +#include "ostree-repo-finder-avahi-private.h" + +/* Reference: RFC 6763, §6. */ +static gboolean +parse_txt_record (const guint8 *txt, + gsize txt_len, + const gchar **key, + gsize *key_len, + const guint8 **value, + gsize *value_len) +{ + gsize i; + + g_return_val_if_fail (key != NULL, FALSE); + g_return_val_if_fail (key_len != NULL, FALSE); + g_return_val_if_fail (value != NULL, FALSE); + g_return_val_if_fail (value_len != NULL, FALSE); + + /* RFC 6763, §6.1. */ + if (txt_len > 8900) + return FALSE; + + *key = (const gchar *) txt; + *key_len = 0; + *value = NULL; + *value_len = 0; + + for (i = 0; i < txt_len; i++) + { + if (txt[i] >= 0x20 && txt[i] <= 0x7e && txt[i] != '=') + { + /* Key character. */ + *key_len = *key_len + 1; + continue; + } + else if (*key_len > 0 && txt[i] == '=') + { + /* Separator. */ + *value = txt + (i + 1); + *value_len = txt_len - (i + 1); + return TRUE; + } + else + { + return FALSE; + } + } + + /* The entire TXT record is the key; there is no ‘=’ or value. */ + *value = NULL; + *value_len = 0; + + return (*key_len > 0); +} + +/* TODO: Docs. Return value is only valid as long as @txt is. Reference: RFC 6763, §6. */ +GHashTable * +_ostree_txt_records_parse (AvahiStringList *txt) +{ + AvahiStringList *l; + g_autoptr(GHashTable) out = NULL; + + out = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_bytes_unref); + + for (l = txt; l != NULL; l = avahi_string_list_get_next (l)) + { + const guint8 *txt; + gsize txt_len; + const gchar *key; + const guint8 *value; + gsize key_len, value_len; + g_autofree gchar *key_allocated = NULL; + g_autoptr(GBytes) value_allocated = NULL; + + txt = avahi_string_list_get_text (l); + txt_len = avahi_string_list_get_size (l); + + if (!parse_txt_record (txt, txt_len, &key, &key_len, &value, &value_len)) + { + g_debug ("Ignoring invalid TXT record of length %" G_GSIZE_FORMAT, + txt_len); + continue; + } + + key_allocated = g_ascii_strdown (key, key_len); + + if (g_hash_table_lookup_extended (out, key_allocated, NULL, NULL)) + { + g_debug ("Ignoring duplicate TXT record ‘%s’", key_allocated); + continue; + } + + /* Distinguish between the case where the entire record is the key + * (value == NULL) and the case where the record is the key + ‘=’ and the + * value is empty (value != NULL && value_len == 0). */ + if (value != NULL) + value_allocated = g_bytes_new_static (value, value_len); + + g_hash_table_insert (out, g_steal_pointer (&key_allocated), g_steal_pointer (&value_allocated)); + } + + return g_steal_pointer (&out); +} diff --git a/src/libostree/ostree-repo-finder-avahi-private.h b/src/libostree/ostree-repo-finder-avahi-private.h new file mode 100644 index 00000000..6429cd7f --- /dev/null +++ b/src/libostree/ostree-repo-finder-avahi-private.h @@ -0,0 +1,35 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2017 Endless Mobile, Inc. + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include +#include +#include + +G_BEGIN_DECLS + +GHashTable *_ostree_txt_records_parse (AvahiStringList *txt); + +G_END_DECLS diff --git a/src/libostree/ostree-repo-finder-avahi.c b/src/libostree/ostree-repo-finder-avahi.c new file mode 100644 index 00000000..433914b4 --- /dev/null +++ b/src/libostree/ostree-repo-finder-avahi.c @@ -0,0 +1,1506 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2016 Kinvolk GmbH + * Copyright © 2017 Endless Mobile, Inc. + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: + * - Krzesimir Nowak + * - Philip Withnall + */ + +#include "config.h" + +#ifdef HAVE_AVAHI +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif /* HAVE_AVAHI */ + +#include +#include +#include + +#include "ostree-autocleanups.h" +#include "ostree-repo-finder.h" +#include "ostree-repo-finder-avahi.h" + +#ifdef HAVE_AVAHI +#include "ostree-bloom-private.h" +#include "ostree-remote-private.h" +#include "ostree-repo-private.h" +#include "ostree-repo.h" +#include "ostree-repo-finder-avahi-private.h" +#include "otutil.h" +#endif /* HAVE_AVAHI */ + +/** + * SECTION:ostree-repo-finder-avahi + * @title: OstreeRepoFinderAvahi + * @short_description: Finds remote repositories from ref names by looking at + * adverts of refs from peers on the local network + * @stability: Unstable + * @include: libostree/ostree-repo-finder-avahi.h + * + * #OstreeRepoFinderAvahi is an implementation of #OstreeRepoFinder which looks + * for refs being hosted by peers on the local network. + * + * Any ref which matches by collection ID and ref name is returned as a result, + * with no limitations on the peers which host them, as long as they are + * accessible over the local network, and their adverts reach this machine via + * DNS-SD/mDNS. + * + * For each repository which is found, a result will be returned for the + * intersection of the refs being searched for, and the refs in `refs/mirrors` + * in the remote repository. + * + * DNS-SD resolution is performed using Avahi, which will continue to scan for + * matching peers throughout the lifetime of the process. It’s recommended that + * ostree_repo_finder_avahi_start() be called early on in the process’ lifetime, + * and the #GMainContext which is passed to ostree_repo_finder_avahi_new() + * continues to be iterated until ostree_repo_finder_avahi_stop() is called. + * + * The values stored in DNS-SD TXT records are stored as big-endian whenever + * endianness is relevant. + * + * Internally, #OstreeRepoFinderAvahi has an Avahi client, browser and resolver + * which work in the background to track all available peers on the local + * network. Whenever a resolve request is made using + * ostree_repo_finder_resolve_async(), the request is blocked until the + * background tracking is in a consistent state (typically this only happens at + * startup), and is then answered using the current cache of background data. + * The Avahi client tracks the #OstreeRepoFinderAvahi’s connection with the + * Avahi D-Bus service. The browser looks for DNS-SD peers on the local network; + * and the resolver is used to retrieve information about services advertised by + * each peer, including the services’ TXT records. + * + * Since: 2017.8 + */ + +#ifdef HAVE_AVAHI +/* FIXME: Submit these upstream */ +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiClient, avahi_client_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiServiceBrowser, avahi_service_browser_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiServiceResolver, avahi_service_resolver_free) + +/* FIXME: Register this with IANA? https://tools.ietf.org/html/rfc6335#section-5.2 */ +const gchar * const OSTREE_AVAHI_SERVICE_TYPE = "_ostree_repo._tcp"; + +static const gchar * +ostree_avahi_client_state_to_string (AvahiClientState state) +{ + switch (state) + { + case AVAHI_CLIENT_S_REGISTERING: + return "registering"; + case AVAHI_CLIENT_S_RUNNING: + return "running"; + case AVAHI_CLIENT_S_COLLISION: + return "collision"; + case AVAHI_CLIENT_CONNECTING: + return "connecting"; + case AVAHI_CLIENT_FAILURE: + return "failure"; + default: + return "unknown"; + } +} + +static const gchar * +ostree_avahi_resolver_event_to_string (AvahiResolverEvent event) +{ + switch (event) + { + case AVAHI_RESOLVER_FOUND: + return "found"; + case AVAHI_RESOLVER_FAILURE: + return "failure"; + default: + return "unknown"; + } +} + +static const gchar * +ostree_avahi_browser_event_to_string (AvahiBrowserEvent event) +{ + switch (event) + { + case AVAHI_BROWSER_NEW: + return "new"; + case AVAHI_BROWSER_REMOVE: + return "remove"; + case AVAHI_BROWSER_CACHE_EXHAUSTED: + return "cache-exhausted"; + case AVAHI_BROWSER_ALL_FOR_NOW: + return "all-for-now"; + case AVAHI_BROWSER_FAILURE: + return "failure"; + default: + return "unknown"; + } +} + +typedef struct +{ + gchar *uri; + gchar *keyring; +} UriAndKeyring; + +static void +uri_and_keyring_free (UriAndKeyring *data) +{ + g_free (data->uri); + g_free (data->keyring); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UriAndKeyring, uri_and_keyring_free) + +static UriAndKeyring * +uri_and_keyring_new (const gchar *uri, + const gchar *keyring) +{ + g_autoptr(UriAndKeyring) data = NULL; + + data = g_new0 (UriAndKeyring, 1); + data->uri = g_strdup (uri); + data->keyring = g_strdup (keyring); + + return g_steal_pointer (&data); +} + +static guint +uri_and_keyring_hash (gconstpointer key) +{ + const UriAndKeyring *_key = key; + + return g_str_hash (_key->uri) ^ g_str_hash (_key->keyring); +} + +static gboolean +uri_and_keyring_equal (gconstpointer a, + gconstpointer b) +{ + const UriAndKeyring *_a = a, *_b = b; + + return g_str_equal (_a->uri, _b->uri) && g_str_equal (_a->keyring, _b->keyring); +} + +/* This must return a valid remote name (suitable for use in a refspec). */ +static gchar * +uri_and_keyring_to_name (UriAndKeyring *data) +{ + g_autofree gchar *escaped_uri = g_uri_escape_string (data->uri, NULL, FALSE); + g_autofree gchar *escaped_keyring = g_uri_escape_string (data->keyring, NULL, FALSE); + + /* FIXME: Need a better separator than `_`, since it’s not escaped in the input. */ + g_autofree gchar *out = g_strdup_printf ("%s_%s", escaped_uri, escaped_keyring); + + for (gsize i = 0; out[i] != '\0'; i++) + { + if (out[i] == '%') + out[i] = '_'; + } + + g_return_val_if_fail (ostree_validate_remote_name (out, NULL), NULL); + + return g_steal_pointer (&out); +} + +/* Internal structure representing a service found advertised by a peer on the + * local network. This includes details for connecting to the service, and the + * metadata associated with the advert (@txt). */ +typedef struct +{ + gchar *name; + gchar *domain; + gchar *address; + guint16 port; + AvahiStringList *txt; +} OstreeAvahiService; + +static void +ostree_avahi_service_free (OstreeAvahiService *service) +{ + g_free (service->name); + g_free (service->domain); + g_free (service->address); + avahi_string_list_free (service->txt); + g_free (service); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeAvahiService, ostree_avahi_service_free) + +/* Convert an AvahiAddress to a string which is suitable for use in URIs (for + * example). Take into account the scope ID, if the address is IPv6 and a + * link-local address. + * (See https://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices and + * https://github.com/lathiat/avahi/issues/110.) */ +static gchar * +address_to_string (const AvahiAddress *address, + AvahiIfIndex interface) +{ + char address_string[AVAHI_ADDRESS_STR_MAX]; + + avahi_address_snprint (address_string, sizeof (address_string), address); + + switch (address->proto) + { + case AVAHI_PROTO_INET6: + if (IN6_IS_ADDR_LINKLOCAL (address->data.data) || + IN6_IS_ADDR_LOOPBACK (address->data.data)) + return g_strdup_printf ("%s%%%d", address_string, interface); + /* else fall through */ + case AVAHI_PROTO_INET: + case AVAHI_PROTO_UNSPEC: + default: + return g_strdup (address_string); + } +} + +static OstreeAvahiService * +ostree_avahi_service_new (const gchar *name, + const gchar *domain, + const AvahiAddress *address, + AvahiIfIndex interface, + guint16 port, + AvahiStringList *txt) +{ + g_autoptr(OstreeAvahiService) service = NULL; + + g_return_val_if_fail (name != NULL, NULL); + g_return_val_if_fail (domain != NULL, NULL); + g_return_val_if_fail (address != NULL, NULL); + g_return_val_if_fail (port > 0, NULL); + + service = g_new0 (OstreeAvahiService, 1); + + service->name = g_strdup (name); + service->domain = g_strdup (domain); + service->address = address_to_string (address, interface); + service->port = port; + service->txt = avahi_string_list_copy (txt); + + return g_steal_pointer (&service); +} + +/* Check whether @str is entirely lower case. */ +static gboolean +str_is_lowercase (const gchar *str) +{ + gsize i; + + for (i = 0; str[i] != '\0'; i++) + { + if (!g_ascii_islower (str[i])) + return FALSE; + } + + return TRUE; +} + +/* Look up @key in the @attributes table derived from a TXT record, and validate + * that its value is of type @value_type. If the key is not found, or its value + * is of the wrong type or is not in normal form, %NULL is returned. @key must + * be lowercase in order to match reliably. */ +static GVariant * +_ostree_txt_records_lookup_variant (GHashTable *attributes, + const gchar *key, + const GVariantType *value_type) +{ + GBytes *value; + g_autoptr(GVariant) variant = NULL; + + g_return_val_if_fail (attributes != NULL, NULL); + g_return_val_if_fail (str_is_lowercase (key), NULL); + g_return_val_if_fail (value_type != NULL, NULL); + + value = g_hash_table_lookup (attributes, key); + + if (value == NULL) + { + g_debug ("TXT attribute ‘%s’ not found.", key); + return NULL; + } + + variant = g_variant_new_from_bytes (value_type, value, FALSE); + + if (!g_variant_is_normal_form (variant)) + { + g_debug ("TXT attribute ‘%s’ value is not in normal form. Ignoring.", key); + return NULL; + } + + return g_steal_pointer (&variant); +} + +/* Bloom hash function family for #OstreeCollectionRef, parameterised by @k. */ +static guint64 +ostree_collection_ref_bloom_hash (gconstpointer element, + guint8 k) +{ + const OstreeCollectionRef *ref = element; + + return ostree_str_bloom_hash (ref->collection_id, k) ^ ostree_str_bloom_hash (ref->ref_name, k); +} + +/* Return the (possibly empty) subset of @refs which are possibly in the given + * encoded bloom filter, @bloom_encoded. The returned array is not + * %NULL-terminated. If there is an error decoding the bloom filter (invalid + * type, zero length, unknown hash function), %NULL will be returned. */ +static GPtrArray * +bloom_refs_intersection (GVariant *bloom_encoded, + const OstreeCollectionRef * const *refs) +{ + g_autoptr(OstreeBloom) bloom = NULL; + g_autoptr(GVariant) bloom_variant = NULL; + guint8 k, hash_id; + OstreeBloomHashFunc hash_func; + const guint8 *bloom_bytes; + gsize n_bloom_bytes; + g_autoptr(GBytes) bytes = NULL; + gsize i; + g_autoptr(GPtrArray) possible_refs = NULL; /* (element-type OstreeCollectionRef) */ + + g_variant_get (bloom_encoded, "(yy@ay)", &k, &hash_id, &bloom_variant); + + if (k == 0) + return NULL; + + switch (hash_id) + { + case 1: + hash_func = ostree_collection_ref_bloom_hash; + break; + default: + return NULL; + } + + bloom_bytes = g_variant_get_fixed_array (bloom_variant, &n_bloom_bytes, sizeof (guint8)); + bytes = g_bytes_new_static (bloom_bytes, n_bloom_bytes); + bloom = ostree_bloom_new_from_bytes (bytes, k, hash_func); + + possible_refs = g_ptr_array_new_with_free_func (NULL); + + for (i = 0; refs[i] != NULL; i++) + { + if (ostree_bloom_maybe_contains (bloom, refs[i])) + g_ptr_array_add (possible_refs, (gpointer) refs[i]); + } + + return g_steal_pointer (&possible_refs); +} + +/* Given a @summary_map of ref name to commit details, and the @collection_id + * for all the refs in the @summary_map (which may be %NULL if the summary does + * not specify one), add the refs to @refs_and_checksums. + * + * The @summary_map is validated as it’s iterated over; on error, @error will be + * set and @refs_and_checksums will be left in an undefined state. */ +static gboolean +fill_refs_and_checksums_from_summary_map (GVariantIter *summary_map, + const gchar *collection_id, + GHashTable *refs_and_checksums /* (element-type OstreeCollectionRef utf8) */, + GError **error) +{ + g_autofree gchar *ref_name = NULL; + g_autoptr(GVariant) checksum_variant = NULL; + + while (g_variant_iter_next (summary_map, "(s(t@aya{sv}))", + (gpointer *) &ref_name, NULL, + (gpointer *) &checksum_variant, NULL)) + { + const OstreeCollectionRef ref = { (gchar *) collection_id, ref_name }; + + if (!ostree_validate_rev (ref_name, error)) + return FALSE; + if (!ostree_validate_structureof_csum_v (checksum_variant, error)) + return FALSE; + + if (g_hash_table_contains (refs_and_checksums, &ref)) + { + g_autofree gchar *checksum_string = ostree_checksum_from_bytes_v (checksum_variant); + + g_hash_table_replace (refs_and_checksums, + ostree_collection_ref_dup (&ref), + g_steal_pointer (&checksum_string)); + } + } + + return TRUE; +} + +/* Given a @summary file, add the refs it lists to @refs_and_checksums. This + * includes the main refs list in the summary, and the map of collection IDs + * to further refs lists. + * + * The @summary is validated as it’s explored; on error, @error will be + * set and @refs_and_checksums will be left in an undefined state. */ +static gboolean +fill_refs_and_checksums_from_summary (GVariant *summary, + GHashTable *refs_and_checksums /* (element-type OstreeCollectionRef utf8) */, + GError **error) +{ + g_autoptr(GVariant) ref_map_v = NULL; + g_autoptr(GVariant) additional_metadata_v = NULL; + GVariantIter ref_map; + g_auto(GVariantDict) additional_metadata = OT_VARIANT_BUILDER_INITIALIZER; + const gchar *collection_id; + g_autoptr(GVariantIter) collection_map = NULL; + + ref_map_v = g_variant_get_child_value (summary, 0); + additional_metadata_v = g_variant_get_child_value (summary, 1); + + g_variant_iter_init (&ref_map, ref_map_v); + g_variant_dict_init (&additional_metadata, additional_metadata_v); + + /* If the summary file specifies a collection ID (to apply to all the refs in its + * ref map), use that to start matching against the queried refs. Otherwise, + * it might specify all its refs in a collection-map; or the summary format is + * old and unsuitable for P2P redistribution and we should bail. */ + if (g_variant_dict_lookup (&additional_metadata, OSTREE_SUMMARY_COLLECTION_ID, "&s", &collection_id)) + { + if (!ostree_validate_collection_id (collection_id, error)) + return FALSE; + if (!fill_refs_and_checksums_from_summary_map (&ref_map, collection_id, refs_and_checksums, error)) + return FALSE; + } + + /* Repeat for the other collections listed in the summary. */ + if (g_variant_dict_lookup (&additional_metadata, OSTREE_SUMMARY_COLLECTION_MAP, "a{sa(s(taya{sv}))}", &collection_map)) + { + while (g_variant_iter_loop (collection_map, "{sa(s(taya{sv}))}", &collection_id, &ref_map)) + { + if (!ostree_validate_collection_id (collection_id, error)) + return FALSE; + if (!fill_refs_and_checksums_from_summary_map (&ref_map, collection_id, refs_and_checksums, error)) + return FALSE; + } + } + + return TRUE; +} + +/* Given a summary file (@summary_bytes), extract the refs it lists, and use that + * to fill in the checksums in the @supported_ref_to_checksum map. This includes + * the main refs list in the summary, and the map of collection IDs to further + * refs lists. + * + * The @summary is validated as it’s explored; on error, @error will be + * set and %FALSE will be returned. If the intersection of the summary file refs + * and the keys in @supported_ref_to_checksum is empty, an error is set. */ +static gboolean +get_refs_and_checksums_from_summary (GBytes *summary_bytes, + GHashTable *supported_ref_to_checksum /* (element-type OstreeCollectionRef utf8) */, + GError **error) +{ + g_autoptr(GVariant) summary = g_variant_ref_sink (g_variant_new_from_bytes (OSTREE_SUMMARY_GVARIANT_FORMAT, summary_bytes, FALSE)); + GHashTableIter iter; + const OstreeCollectionRef *ref; + const gchar *checksum; + + if (!g_variant_is_normal_form (summary)) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Not normal form"); + return FALSE; + } + if (!g_variant_is_of_type (summary, OSTREE_SUMMARY_GVARIANT_FORMAT)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Doesn't match variant type '%s'", + (char *)OSTREE_SUMMARY_GVARIANT_FORMAT); + return FALSE; + } + + if (!fill_refs_and_checksums_from_summary (summary, supported_ref_to_checksum, error)) + return FALSE; + + /* Check that at least one of the refs has a non-%NULL checksum set, otherwise + * we can discard this peer. */ + g_hash_table_iter_init (&iter, supported_ref_to_checksum); + while (g_hash_table_iter_next (&iter, + (gpointer *) &ref, + (gpointer *) &checksum)) + { + if (checksum != NULL) + return TRUE; + } + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No matching refs were found in the summary file"); + return FALSE; +} + +/* Download the summary file from @remote, and return the bytes of the file in + * @out_summary_bytes. */ +static gboolean +fetch_summary_from_remote (OstreeRepo *repo, + OstreeRemote *remote, + GBytes **out_summary_bytes, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GBytes) summary_bytes = NULL; + gboolean remote_already_existed = _ostree_repo_add_remote (repo, remote); + gboolean success = ostree_repo_remote_fetch_summary_with_options (repo, + remote->name, + NULL /* options */, + &summary_bytes, + NULL /* signature */, + cancellable, + error); + + if (!remote_already_existed) + _ostree_repo_remove_remote (repo, remote); + + if (!success) + return FALSE; + + g_assert (out_summary_bytes != NULL); + *out_summary_bytes = g_steal_pointer (&summary_bytes); + return TRUE; +} +#endif /* HAVE_AVAHI */ + +struct _OstreeRepoFinderAvahi +{ + GObject parent_instance; + +#ifdef HAVE_AVAHI + /* All elements of this structure must only be accessed from @avahi_context + * after construction. */ + + /* Note: There is a ref-count loop here: each #GTask has a reference to the + * #OstreeRepoFinderAvahi, and we have to keep a reference to the #GTask. */ + GPtrArray *resolve_tasks; /* (element-type (owned) GTask) */ + + AvahiGLibPoll *poll; + AvahiClient *client; + AvahiServiceBrowser *browser; + + AvahiClientState client_state; + gboolean browser_failed; + gboolean browser_all_for_now; + + GCancellable *avahi_cancellable; + GMainContext *avahi_context; + + /* Map of service name (typically human readable) to a #GPtrArray of the + * #AvahiServiceResolver instances we have running against that name. We + * could end up with more than one resolver if the same name is advertised to + * us over multiple interfaces or protocols (for example, IPv4 and IPv6). + * Resolve all of them just in case one doesn’t work. */ + GHashTable *resolvers; /* (element-type (owned) utf8 (owned) GPtrArray (element-type (owned) AvahiServiceResolver)) */ + + /* Array of #OstreeAvahiService instances representing all the services which + * we currently think are valid. */ + GPtrArray *found_services; /* (element-type (owned OstreeAvahiService) */ +#endif /* HAVE_AVAHI */ +}; + +static void ostree_repo_finder_avahi_iface_init (OstreeRepoFinderInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (OstreeRepoFinderAvahi, ostree_repo_finder_avahi, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_REPO_FINDER, ostree_repo_finder_avahi_iface_init)) + +#ifdef HAVE_AVAHI + +/* Download the summary file from @remote and fill in the checksums in the given + * @supported_ref_to_checksum hash table, given the existing refs in it as keys. + * See get_refs_and_checksums_from_summary() for more details. */ +static gboolean +get_checksums (OstreeRepoFinderAvahi *finder, + OstreeRepo *repo, + OstreeRemote *remote, + GHashTable *supported_ref_to_checksum /* (element-type OstreeCollectionRef utf8) */, + GError **error) +{ + g_autoptr(GBytes) summary_bytes = NULL; + + if (!fetch_summary_from_remote (repo, + remote, + &summary_bytes, + finder->avahi_cancellable, + error)) + return FALSE; + + return get_refs_and_checksums_from_summary (summary_bytes, supported_ref_to_checksum, error); +} + +/* Build some #OstreeRepoFinderResults out of the given #OstreeAvahiService by + * parsing its DNS-SD TXT records and finding the intersection between the refs + * it advertises and @refs. One or more results will be added to @results, with + * multiple results being added if the intersection of refs covers refs which + * need different GPG keyrings. One result is added per (uri, keyring) pair. + * + * If any of the TXT records are malformed or missing, or if the intersection of + * refs is empty, return early without modifying @results. + * + * This recognises the following TXT records: + * - `v` (`y`): Version of the TXT record format. Only version `1` is currently + * supported. + * - `rb` (`(yyay)`): Bloom filter indicating which refs are served by the peer. + * - `st` (`t`): Timestamp (seconds since the Unix epoch, big endian) the + * summary file was last modified. + * - `ri` (`q`): Repository index, indicating which of several repositories + * hosted on the peer this is. Big endian. + */ +static void +ostree_avahi_service_build_repo_finder_result (OstreeAvahiService *service, + OstreeRepoFinderAvahi *finder, + OstreeRepo *parent_repo, + gint priority, + const OstreeCollectionRef * const *refs, + GPtrArray *results, + GCancellable *cancellable) +{ + g_autoptr(GHashTable) attributes = NULL; + g_autoptr(GVariant) version = NULL; + g_autoptr(GVariant) bloom = NULL; + g_autoptr(GVariant) summary_timestamp = NULL; + g_autoptr(GVariant) repo_index = NULL; + g_autofree gchar *repo_path = NULL; + g_autoptr(GPtrArray) possible_refs = NULL; /* (element-type OstreeCollectionRef) */ + SoupURI *_uri = NULL; + g_autofree gchar *uri = NULL; + g_autoptr(GError) error = NULL; + gsize i; + g_autoptr(GHashTable) repo_to_refs = NULL; /* (element-type UriAndKeyring GHashTable) */ + GHashTable *supported_ref_to_checksum; /* (element-type OstreeCollectionRef utf8) */ + GHashTableIter iter; + UriAndKeyring *repo; + + g_return_if_fail (service != NULL); + g_return_if_fail (refs != NULL); + + attributes = _ostree_txt_records_parse (service->txt); + + /* Check the record version. */ + version = _ostree_txt_records_lookup_variant (attributes, "v", G_VARIANT_TYPE_BYTE); + + if (g_variant_get_byte (version) != 1) + { + g_debug ("Unknown v=%02x attribute provided in TXT record. Ignoring.", + g_variant_get_byte (version)); + return; + } + + /* Refs bloom filter? */ + bloom = _ostree_txt_records_lookup_variant (attributes, "rb", G_VARIANT_TYPE ("(yyay)")); + + if (bloom == NULL) + { + g_debug ("Missing rb (refs bloom) attribute in TXT record. Ignoring."); + return; + } + + possible_refs = bloom_refs_intersection (bloom, refs); + if (possible_refs == NULL) + { + g_debug ("Wrong k parameter or hash id in rb (refs bloom) attribute in TXT record. Ignoring."); + return; + } + if (possible_refs->len == 0) + { + g_debug ("TXT record definitely has no matching refs. Ignoring."); + return; + } + + /* Summary timestamp. */ + summary_timestamp = _ostree_txt_records_lookup_variant (attributes, "st", G_VARIANT_TYPE_UINT64); + if (summary_timestamp == NULL) + { + g_debug ("Missing st (summary timestamp) attribute in TXT record. Ignoring."); + return; + } + + /* Repository index. */ + repo_index = _ostree_txt_records_lookup_variant (attributes, "ri", G_VARIANT_TYPE_UINT16); + if (repo_index == NULL) + { + g_debug ("Missing ri (repository index) attribute in TXT record. Ignoring."); + return; + } + repo_path = g_strdup_printf ("/%u", GUINT16_FROM_BE (g_variant_get_uint16 (repo_index))); + + /* Create a new result for each keyring needed by @possible_refs. Typically, + * there will be a separate keyring per collection, but some might be shared. */ + repo_to_refs = g_hash_table_new_full (uri_and_keyring_hash, uri_and_keyring_equal, + (GDestroyNotify) uri_and_keyring_free, (GDestroyNotify) g_hash_table_unref); + + _uri = soup_uri_new (NULL); + soup_uri_set_scheme (_uri, "http"); + soup_uri_set_host (_uri, service->address); + soup_uri_set_port (_uri, service->port); + soup_uri_set_path (_uri, repo_path); + uri = soup_uri_to_string (_uri, FALSE); + soup_uri_free (_uri); + + for (i = 0; i < possible_refs->len; i++) + { + const OstreeCollectionRef *ref = g_ptr_array_index (possible_refs, i); + g_autofree gchar *keyring = NULL; + g_autoptr(UriAndKeyring) resolved_repo = NULL; + + /* Look up the GPG keyring for this ref. */ + keyring = ostree_repo_resolve_keyring_for_collection (parent_repo, ref->collection_id, + cancellable, &error); + + if (keyring == NULL) + { + g_debug ("Ignoring ref (%s, %s) on host ‘%s’ due to missing keyring: %s", + ref->collection_id, refs[i]->ref_name, service->address, + error->message); + g_clear_error (&error); + continue; + } + + /* Add this repo to the results, keyed by the canonicalised repository URI + * to deduplicate the results. */ + g_debug ("Resolved ref (%s, %s) to repo URI ‘%s’ with keyring ‘%s’.", + ref->collection_id, ref->ref_name, uri, keyring); + + resolved_repo = uri_and_keyring_new (uri, keyring); + + supported_ref_to_checksum = g_hash_table_lookup (repo_to_refs, resolved_repo); + + if (supported_ref_to_checksum == NULL) + { + supported_ref_to_checksum = g_hash_table_new_full (ostree_collection_ref_hash, + ostree_collection_ref_equal, + NULL, g_free); + g_hash_table_insert (repo_to_refs, g_steal_pointer (&resolved_repo), supported_ref_to_checksum /* transfer */); + } + + /* Add a placeholder to @supported_ref_to_checksum for this ref. It will + * be filled out by the get_checksums() call below. */ + g_hash_table_insert (supported_ref_to_checksum, (gpointer) ref, NULL); + } + + /* Aggregate the results. */ + g_hash_table_iter_init (&iter, repo_to_refs); + + while (g_hash_table_iter_next (&iter, (gpointer *) &repo, (gpointer *) &supported_ref_to_checksum)) + { + g_autoptr(OstreeRemote) remote = NULL; + + /* Build an #OstreeRemote. Use the escaped URI, since remote->name + * is used in file paths, so needs to not contain special characters. */ + g_autofree gchar *name = uri_and_keyring_to_name (repo); + remote = ostree_remote_new (name); + + g_clear_pointer (&remote->keyring, g_free); + remote->keyring = g_strdup (repo->keyring); + + g_key_file_set_string (remote->options, remote->group, "url", repo->uri); + g_key_file_set_boolean (remote->options, remote->group, "gpg-verify", TRUE); + g_key_file_set_boolean (remote->options, remote->group, "gpg-verify-summary", TRUE); + + get_checksums (finder, parent_repo, remote, supported_ref_to_checksum, &error); + if (error != NULL) + { + g_debug ("Failed to get checksums for possible refs; ignoring: %s", error->message); + g_clear_error (&error); + continue; + } + + g_ptr_array_add (results, ostree_repo_finder_result_new (remote, OSTREE_REPO_FINDER (finder), + priority, supported_ref_to_checksum, + (summary_timestamp != NULL) ? GUINT64_FROM_BE (g_variant_get_uint64 (summary_timestamp)) : 0)); + } +} + +typedef struct +{ + OstreeCollectionRef **refs; /* (owned) (array zero-terminated=1) */ + OstreeRepo *parent_repo; /* (owned) */ +} ResolveData; + +static void +resolve_data_free (ResolveData *data) +{ + g_object_unref (data->parent_repo); + ostree_collection_ref_freev (data->refs); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ResolveData, resolve_data_free) + +static ResolveData * +resolve_data_new (const OstreeCollectionRef * const *refs, + OstreeRepo *parent_repo) +{ + g_autoptr(ResolveData) data = NULL; + + data = g_new0 (ResolveData, 1); + data->refs = ostree_collection_ref_dupv (refs); + data->parent_repo = g_object_ref (parent_repo); + + return g_steal_pointer (&data); +} + +static void +fail_all_pending_tasks (OstreeRepoFinderAvahi *self, + GQuark domain, + gint code, + const gchar *format, + ...) G_GNUC_PRINTF(4, 5); + +/* Executed in @self->avahi_context. + * + * Return the given error from all the pending resolve tasks in + * self->resolve_tasks. */ +static void +fail_all_pending_tasks (OstreeRepoFinderAvahi *self, + GQuark domain, + gint code, + const gchar *format, + ...) +{ + gsize i; + va_list args; + g_autoptr(GError) error = NULL; + + g_assert (g_main_context_is_owner (self->avahi_context)); + + va_start (args, format); + error = g_error_new_valist (domain, code, format, args); + va_end (args); + + for (i = 0; i < self->resolve_tasks->len; i++) + { + GTask *task = G_TASK (g_ptr_array_index (self->resolve_tasks, i)); + g_task_return_error (task, g_error_copy (error)); + } + + g_ptr_array_set_size (self->resolve_tasks, 0); +} + +static gint +results_compare_cb (gconstpointer a, + gconstpointer b) +{ + const OstreeRepoFinderResult *result_a = *((const OstreeRepoFinderResult **) a); + const OstreeRepoFinderResult *result_b = *((const OstreeRepoFinderResult **) b); + + return ostree_repo_finder_result_compare (result_a, result_b); +} + +/* Executed in @self->avahi_context. + * + * For each of the pending resolve tasks in self->resolve_tasks, calculate and + * return the result set for its query given the currently known services from + * Avahi which are stored in self->found_services. */ +static void +complete_all_pending_tasks (OstreeRepoFinderAvahi *self) +{ + gsize i; + const gint priority = 60; /* arbitrarily chosen */ + g_autoptr(GPtrArray) results_for_tasks = g_ptr_array_new_full (self->resolve_tasks->len, (GDestroyNotify)g_ptr_array_unref); + gboolean cancelled = FALSE; + + g_assert (g_main_context_is_owner (self->avahi_context)); + g_debug ("%s: Completing %u tasks", G_STRFUNC, self->resolve_tasks->len); + + for (i = 0; i < self->resolve_tasks->len; i++) + { + g_autoptr(GPtrArray) results = NULL; + GTask *task; + ResolveData *data; + const OstreeCollectionRef * const *refs; + gsize j; + + task = G_TASK (g_ptr_array_index (self->resolve_tasks, i)); + data = g_task_get_task_data (task); + refs = (const OstreeCollectionRef * const *) data->refs; + results = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_repo_finder_result_free); + + for (j = 0; j < self->found_services->len; j++) + { + OstreeAvahiService *service = g_ptr_array_index (self->found_services, j); + + ostree_avahi_service_build_repo_finder_result (service, self, data->parent_repo, + priority, refs, results, + self->avahi_cancellable); + if (g_cancellable_is_cancelled (self->avahi_cancellable)) + { + cancelled = TRUE; + break; + } + } + if (cancelled) + break; + + g_ptr_array_add (results_for_tasks, g_steal_pointer (&results)); + } + + if (!cancelled) + { + for (i = 0; i < self->resolve_tasks->len; i++) + { + GTask *task = G_TASK (g_ptr_array_index (self->resolve_tasks, i)); + GPtrArray *results = g_ptr_array_index (results_for_tasks, i); + + g_ptr_array_sort (results, results_compare_cb); + + g_task_return_pointer (task, + g_ptr_array_ref (results), + (GDestroyNotify) g_ptr_array_unref); + } + + g_ptr_array_set_size (self->resolve_tasks, 0); + } + else + { + fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "Avahi service resolution cancelled."); + } +} + +/* Executed in @self->avahi_context. */ +static void +maybe_complete_all_pending_tasks (OstreeRepoFinderAvahi *self) +{ + g_assert (g_main_context_is_owner (self->avahi_context)); + g_debug ("%s: client_state: %s, browser_failed: %u, cancelled: %u, " + "browser_all_for_now: %u, n_resolvers: %u", + G_STRFUNC, ostree_avahi_client_state_to_string (self->client_state), + self->browser_failed, + g_cancellable_is_cancelled (self->avahi_cancellable), + self->browser_all_for_now, g_hash_table_size (self->resolvers)); + + if (self->client_state == AVAHI_CLIENT_FAILURE) + fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_FAILED, + "Avahi client error: %s", + avahi_strerror (avahi_client_errno (self->client))); + else if (self->browser_failed) + fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_FAILED, + "Avahi browser error: %s", + avahi_strerror (avahi_client_errno (self->client))); + else if (g_cancellable_is_cancelled (self->avahi_cancellable)) + fail_all_pending_tasks (self, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "Avahi service resolution cancelled."); + else if (self->browser_all_for_now && + g_hash_table_size (self->resolvers) == 0) + complete_all_pending_tasks (self); +} + +/* Executed in @self->avahi_context. */ +static void +client_cb (AvahiClient *client, + AvahiClientState state, + void *finder_ptr) +{ + /* Completing the pending tasks might drop the final reference to @self. */ + g_autoptr(OstreeRepoFinderAvahi) self = g_object_ref (finder_ptr); + + /* self->client will be NULL if client_cb() is called from + * ostree_repo_finder_avahi_start(). */ + g_assert (self->client == NULL || g_main_context_is_owner (self->avahi_context)); + + g_debug ("%s: Entered state ‘%s’.", + G_STRFUNC, ostree_avahi_client_state_to_string (state)); + + /* We only care about entering and leaving %AVAHI_CLIENT_FAILURE. */ + self->client_state = state; + if (self->client != NULL) + maybe_complete_all_pending_tasks (self); +} + +/* Executed in @self->avahi_context. */ +static void +resolve_cb (AvahiServiceResolver *resolver, + AvahiIfIndex interface, + AvahiProtocol protocol, + AvahiResolverEvent event, + const char *name, + const char *type, + const char *domain, + const char *host_name, + const AvahiAddress *address, + uint16_t port, + AvahiStringList *txt, + AvahiLookupResultFlags flags, + void *finder_ptr) +{ + /* Completing the pending tasks might drop the final reference to @self. */ + g_autoptr(OstreeRepoFinderAvahi) self = g_object_ref (finder_ptr); + g_autoptr(OstreeAvahiService) service = NULL; + GPtrArray *resolvers; + + g_assert (g_main_context_is_owner (self->avahi_context)); + + g_debug ("%s: Resolve event ‘%s’ for name ‘%s’.", + G_STRFUNC, ostree_avahi_resolver_event_to_string (event), name); + + /* Track the resolvers active for this @name. There may be several, + * as @name might appear to us over several interfaces or protocols. Most + * commonly this happens when both hosts are connected via IPv4 and IPv6. */ + resolvers = g_hash_table_lookup (self->resolvers, name); + + if (resolvers == NULL || resolvers->len == 0) + { + /* maybe it was removed in the meantime */ + g_hash_table_remove (self->resolvers, name); + return; + } + else if (resolvers->len == 1) + { + g_hash_table_remove (self->resolvers, name); + } + else + { + g_ptr_array_remove_fast (resolvers, resolver); + } + + /* Was resolution successful? */ + switch (event) + { + case AVAHI_RESOLVER_FOUND: + service = ostree_avahi_service_new (name, domain, address, interface, + port, txt); + g_ptr_array_add (self->found_services, g_steal_pointer (&service)); + break; + case AVAHI_RESOLVER_FAILURE: + default: + g_warning ("Failed to resolve service ‘%s’: %s", name, + avahi_strerror (avahi_client_errno (self->client))); + break; + } + + maybe_complete_all_pending_tasks (self); +} + +/* Executed in @self->avahi_context. */ +static void +browse_new (OstreeRepoFinderAvahi *self, + AvahiIfIndex interface, + AvahiProtocol protocol, + const gchar *name, + const gchar *type, + const gchar *domain) +{ + g_autoptr(AvahiServiceResolver) resolver = NULL; + GPtrArray *resolvers; /* (element-type AvahiServiceResolver) */ + + g_assert (g_main_context_is_owner (self->avahi_context)); + + resolver = avahi_service_resolver_new (self->client, + interface, + protocol, + name, + type, + domain, + AVAHI_PROTO_UNSPEC, + 0, + resolve_cb, + self); + if (resolver == NULL) + { + g_warning ("Failed to resolve service ‘%s’: %s", name, + avahi_strerror (avahi_client_errno (self->client))); + return; + } + + g_debug ("Found name service %s on the network; type: %s, domain: %s, " + "protocol: %u, interface: %u", name, type, domain, protocol, + interface); + + /* Start a resolver for this (interface, protocol, name, type, domain) + * combination. */ + resolvers = g_hash_table_lookup (self->resolvers, name); + if (resolvers == NULL) + { + resolvers = g_ptr_array_new_with_free_func ((GDestroyNotify) avahi_service_resolver_free); + g_hash_table_insert (self->resolvers, g_strdup (name), resolvers); + } + + g_ptr_array_add (resolvers, g_steal_pointer (&resolver)); +} + +/* Executed in @self->avahi_context. Caller must call maybe_complete_all_pending_tasks(). */ +static void +browse_remove (OstreeRepoFinderAvahi *self, + const char *name) +{ + gsize i; + gboolean removed = FALSE; + + g_assert (g_main_context_is_owner (self->avahi_context)); + + g_hash_table_remove (self->resolvers, name); + + for (i = 0; i < self->found_services->len; i += (removed ? 0 : 1)) + { + OstreeAvahiService *service = g_ptr_array_index (self->found_services, i); + + removed = FALSE; + + if (g_strcmp0 (service->name, name) == 0) + { + g_ptr_array_remove_index_fast (self->found_services, i); + removed = TRUE; + continue; + } + } +} + +/* Executed in @self->avahi_context. */ +static void +browse_cb (AvahiServiceBrowser *browser, + AvahiIfIndex interface, + AvahiProtocol protocol, + AvahiBrowserEvent event, + const char *name, + const char *type, + const char *domain, + AvahiLookupResultFlags flags, + void *finder_ptr) +{ + /* Completing the pending tasks might drop the final reference to @self. */ + g_autoptr(OstreeRepoFinderAvahi) self = g_object_ref (finder_ptr); + + g_assert (g_main_context_is_owner (self->avahi_context)); + + g_debug ("%s: Browse event ‘%s’ for name ‘%s’.", + G_STRFUNC, ostree_avahi_browser_event_to_string (event), name); + + self->browser_failed = FALSE; + + switch (event) + { + case AVAHI_BROWSER_NEW: + browse_new (self, interface, protocol, name, type, domain); + break; + + case AVAHI_BROWSER_REMOVE: + browse_remove (self, name); + break; + + case AVAHI_BROWSER_CACHE_EXHAUSTED: + /* don’t care about this. */ + break; + + case AVAHI_BROWSER_ALL_FOR_NOW: + self->browser_all_for_now = TRUE; + break; + + case AVAHI_BROWSER_FAILURE: + self->browser_failed = TRUE; + break; + + default: + g_assert_not_reached (); + } + + /* Check all the tasks for any event, since the @browser_failed state + * may have changed. */ + maybe_complete_all_pending_tasks (self); +} + +static gboolean add_resolve_task_cb (gpointer user_data); +#endif /* HAVE_AVAHI */ + +static void +ostree_repo_finder_avahi_resolve_async (OstreeRepoFinder *finder, + const OstreeCollectionRef * const *refs, + OstreeRepo *parent_repo, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + OstreeRepoFinderAvahi *self = OSTREE_REPO_FINDER_AVAHI (finder); + g_autoptr(GTask) task = NULL; + + g_debug ("%s: Starting resolving", G_STRFUNC); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, ostree_repo_finder_avahi_resolve_async); + +#ifdef HAVE_AVAHI + g_task_set_task_data (task, resolve_data_new (refs, parent_repo), (GDestroyNotify) resolve_data_free); + + /* Move @task to the @avahi_context where it can be processed. */ + g_main_context_invoke (self->avahi_context, add_resolve_task_cb, g_steal_pointer (&task)); +#else /* if !HAVE_AVAHI */ + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Avahi support was not compiled in to libostree"); +#endif /* !HAVE_AVAHI */ +} + +#ifdef HAVE_AVAHI +/* Executed in @self->avahi_context. */ +static gboolean +add_resolve_task_cb (gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + OstreeRepoFinderAvahi *self = g_task_get_source_object (task); + + g_assert (g_main_context_is_owner (self->avahi_context)); + g_debug ("%s", G_STRFUNC); + + /* Track the task and check to see if the browser and resolvers are in a + * quiescent state suitable for returning a result immediately. */ + g_ptr_array_add (self->resolve_tasks, g_object_ref (task)); + maybe_complete_all_pending_tasks (self); + + return G_SOURCE_REMOVE; +} +#endif /* HAVE_AVAHI */ + +static GPtrArray * +ostree_repo_finder_avahi_resolve_finish (OstreeRepoFinder *finder, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, finder), NULL); + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +ostree_repo_finder_avahi_dispose (GObject *obj) +{ +#ifdef HAVE_AVAHI + OstreeRepoFinderAvahi *self = OSTREE_REPO_FINDER_AVAHI (obj); + + ostree_repo_finder_avahi_stop (self); + + g_assert (self->resolve_tasks == NULL || self->resolve_tasks->len == 0); + + g_clear_pointer (&self->resolve_tasks, g_ptr_array_unref); + g_clear_pointer (&self->browser, avahi_service_browser_free); + g_clear_pointer (&self->client, avahi_client_free); + g_clear_pointer (&self->poll, avahi_glib_poll_free); + g_clear_pointer (&self->avahi_context, g_main_context_unref); + g_clear_pointer (&self->found_services, g_ptr_array_unref); + g_clear_pointer (&self->resolvers, g_hash_table_unref); + g_clear_object (&self->avahi_cancellable); +#endif /* HAVE_AVAHI */ + + /* Chain up. */ + G_OBJECT_CLASS (ostree_repo_finder_avahi_parent_class)->dispose (obj); +} + +static void +ostree_repo_finder_avahi_class_init (OstreeRepoFinderAvahiClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = ostree_repo_finder_avahi_dispose; +} + +static void +ostree_repo_finder_avahi_iface_init (OstreeRepoFinderInterface *iface) +{ + iface->resolve_async = ostree_repo_finder_avahi_resolve_async; + iface->resolve_finish = ostree_repo_finder_avahi_resolve_finish; +} + +static void +ostree_repo_finder_avahi_init (OstreeRepoFinderAvahi *self) +{ +#ifdef HAVE_AVAHI + self->resolve_tasks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + self->avahi_cancellable = g_cancellable_new (); + self->client_state = AVAHI_CLIENT_S_REGISTERING; + self->resolvers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); + self->found_services = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_avahi_service_free); +#endif /* HAVE_AVAHI */ +} + +/** + * ostree_repo-finder_avahi_new: + * @context: (transfer none) (nullable): a #GMainContext for processing Avahi + * events in, or %NULL to use the current thread-default + * + * Create a new #OstreeRepoFinderAvahi instance. It is intended that one such + * instance be created per process, and it be used to answer all resolution + * requests from #OstreeRepos. + * + * The calling code is responsible for ensuring that @context is iterated while + * the #OstreeRepoFinderAvahi is running (after ostree_repo_finder_avahi_start() + * is called). This may be done from any thread. + * + * If @context is %NULL, the current thread-default #GMainContext is used. + * + * Returns: (transfer full): a new #OstreeRepoFinderAvahi + * Since: 2017.8 + */ +OstreeRepoFinderAvahi * +ostree_repo_finder_avahi_new (GMainContext *context) +{ + g_autoptr(OstreeRepoFinderAvahi) finder = NULL; + + finder = g_object_new (OSTREE_TYPE_REPO_FINDER_AVAHI, NULL); + +#ifdef HAVE_AVAHI + /* FIXME: Make this a property */ + if (context != NULL) + finder->avahi_context = g_main_context_ref (context); + else + finder->avahi_context = g_main_context_ref_thread_default (); + + /* Avahi setup. Note: Technically the allocator is per-process state which we + * shouldn’t set here, but it’s probably fine. It’s unlikely that code which + * is using libostree is going to use an allocator which is not GLib, and + * *also* use Avahi API itself. */ + avahi_set_allocator (avahi_glib_allocator ()); + finder->poll = avahi_glib_poll_new (finder->avahi_context, G_PRIORITY_DEFAULT); +#endif /* HAVE_AVAHI */ + + return g_steal_pointer (&finder); +} + +/** + * ostree_repo_finder_avahi_start: + * @self: an #OstreeRepoFinderAvahi + * @error: return location for a #GError + * + * Start monitoring the local network for peers who are advertising OSTree + * repositories, using Avahi. In order for this to work, the #GMainContext + * passed to @self at construction time must be iterated (so it will typically + * be the global #GMainContext, or be a separate #GMainContext in a worker + * thread). + * + * This will return an error (%G_IO_ERROR_FAILED) if initialisation fails, or if + * Avahi support is not available (%G_IO_ERROR_NOT_SUPPORTED). In either case, + * the #OstreeRepoFinderAvahi instance is useless afterwards and should be + * destroyed. + * + * Call ostree_repo_finder_avahi_stop() to stop the repo finder. + * + * It is an error to call this function multiple times on the same + * #OstreeRepoFinderAvahi instance, or to call it after + * ostree_repo_finder_avahi_stop(). + * + * Since: 2017.8 + */ +void +ostree_repo_finder_avahi_start (OstreeRepoFinderAvahi *self, + GError **error) +{ + g_return_if_fail (OSTREE_IS_REPO_FINDER_AVAHI (self)); + g_return_if_fail (error == NULL || *error == NULL); + +#ifdef HAVE_AVAHI + g_autoptr(AvahiClient) client = NULL; + g_autoptr(AvahiServiceBrowser) browser = NULL; + int failure = 0; + + if (g_cancellable_set_error_if_cancelled (self->avahi_cancellable, error)) + return; + + g_assert (self->client == NULL); + + client = avahi_client_new (avahi_glib_poll_get (self->poll), + AVAHI_CLIENT_NO_FAIL, + client_cb, self, &failure); + + if (client == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create finder client: %s", + avahi_strerror (failure)); + return; + } + + /* Query for the OSTree DNS-SD service on the local network. */ + browser = avahi_service_browser_new (client, + AVAHI_IF_UNSPEC, + AVAHI_PROTO_UNSPEC, + OSTREE_AVAHI_SERVICE_TYPE, + NULL, + 0, + browse_cb, + self); + + if (browser == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to create service browser: %s", + avahi_strerror (avahi_client_errno (client))); + return; + } + + /* Success. */ + self->client = g_steal_pointer (&client); + self->browser = g_steal_pointer (&browser); +#else /* if !HAVE_AVAHI */ + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Avahi support was not compiled in to libostree"); +#endif /* !HAVE_AVAHI */ +} + +#ifdef HAVE_AVAHI +static gboolean stop_cb (gpointer user_data); +#endif /* HAVE_AVAHI */ + +/** + * ostree_repo_finder_avahi_stop: + * @self: an #OstreeRepoFinderAvahi + * + * Stop monitoring the local network for peers who are advertising OSTree + * repositories. If any resolve tasks (from ostree_repo_finder_resolve_async()) + * are in progress, they will be cancelled and will return %G_IO_ERROR_CANCELLED. + * + * Call ostree_repo_finder_avahi_start() to start the repo finder. + * + * It is an error to call this function multiple times on the same + * #OstreeRepoFinderAvahi instance, or to call it before + * ostree_repo_finder_avahi_start(). + * + * Since: 2017.8 + */ +void +ostree_repo_finder_avahi_stop (OstreeRepoFinderAvahi *self) +{ + g_return_if_fail (OSTREE_IS_REPO_FINDER_AVAHI (self)); + +#ifdef HAVE_AVAHI + if (self->browser == NULL) + return; + + g_main_context_invoke (self->avahi_context, stop_cb, g_object_ref (self)); +#endif /* HAVE_AVAHI */ +} + +#ifdef HAVE_AVAHI +static gboolean +stop_cb (gpointer user_data) +{ + g_autoptr(OstreeRepoFinderAvahi) self = OSTREE_REPO_FINDER_AVAHI (user_data); + + g_cancellable_cancel (self->avahi_cancellable); + maybe_complete_all_pending_tasks (self); + + g_clear_pointer (&self->browser, avahi_service_browser_free); + g_clear_pointer (&self->client, avahi_client_free); + g_hash_table_remove_all (self->resolvers); + + return G_SOURCE_REMOVE; +} +#endif /* HAVE_AVAHI */ diff --git a/src/libostree/ostree-repo-finder-avahi.h b/src/libostree/ostree-repo-finder-avahi.h new file mode 100644 index 00000000..98d37723 --- /dev/null +++ b/src/libostree/ostree-repo-finder-avahi.h @@ -0,0 +1,62 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2017 Endless Mobile, Inc. + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include +#include + +#include "ostree-repo-finder.h" +#include "ostree-types.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_REPO_FINDER_AVAHI (ostree_repo_finder_avahi_get_type ()) + +/* Manually expanded version of the following, omitting autoptr support (for GLib < 2.44): +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeRepoFinderAvahi, ostree_repo_finder_avahi, OSTREE, REPO_FINDER_AVAHI, GObject) */ + +_OSTREE_PUBLIC +GType ostree_repo_finder_avahi_get_type (void); + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +typedef struct _OstreeRepoFinderAvahi OstreeRepoFinderAvahi; +typedef struct { GObjectClass parent_class; } OstreeRepoFinderAvahiClass; + +static inline OstreeRepoFinderAvahi *OSTREE_REPO_FINDER_AVAHI (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_repo_finder_avahi_get_type (), OstreeRepoFinderAvahi); } +static inline gboolean OSTREE_IS_REPO_FINDER_AVAHI (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_repo_finder_avahi_get_type ()); } +G_GNUC_END_IGNORE_DEPRECATIONS + +_OSTREE_PUBLIC +OstreeRepoFinderAvahi *ostree_repo_finder_avahi_new (GMainContext *context); + +_OSTREE_PUBLIC +void ostree_repo_finder_avahi_start (OstreeRepoFinderAvahi *self, + GError **error); + +_OSTREE_PUBLIC +void ostree_repo_finder_avahi_stop (OstreeRepoFinderAvahi *self); + +G_END_DECLS diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c index ecc9b72c..4c87199a 100644 --- a/src/libostree/ostree-repo-pull.c +++ b/src/libostree/ostree-repo-pull.c @@ -43,6 +43,9 @@ #include "ostree-repo-finder.h" #include "ostree-repo-finder-config.h" #include "ostree-repo-finder-mount.h" +#ifdef HAVE_AVAHI +#include "ostree-repo-finder-avahi.h" +#endif /* HAVE_AVAHI */ #endif /* OSTREE_ENABLE_EXPERIMENTAL_API */ #include @@ -3961,11 +3964,13 @@ typedef struct OstreeCollectionRef **refs; GVariant *options; OstreeAsyncProgress *progress; + OstreeRepoFinder *default_finder_avahi; } FindRemotesData; static void find_remotes_data_free (FindRemotesData *data) { + g_clear_object (&data->default_finder_avahi); g_clear_object (&data->progress); g_clear_pointer (&data->options, g_variant_unref); ostree_collection_ref_freev (data->refs); @@ -3978,7 +3983,8 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (FindRemotesData, find_remotes_data_free) static FindRemotesData * find_remotes_data_new (const OstreeCollectionRef * const *refs, GVariant *options, - OstreeAsyncProgress *progress) + OstreeAsyncProgress *progress, + OstreeRepoFinder *default_finder_avahi) { g_autoptr(FindRemotesData) data = NULL; @@ -3986,6 +3992,7 @@ find_remotes_data_new (const OstreeCollectionRef * const *refs, data->refs = ostree_collection_ref_dupv (refs); data->options = (options != NULL) ? g_variant_ref (options) : NULL; data->progress = (progress != NULL) ? g_object_ref (progress) : NULL; + data->default_finder_avahi = (default_finder_avahi != NULL) ? g_object_ref (default_finder_avahi) : NULL; return g_steal_pointer (&data); } @@ -4085,10 +4092,10 @@ ostree_repo_find_remotes_async (OstreeRepo *self, { g_autoptr(GTask) task = NULL; g_autoptr(FindRemotesData) data = NULL; - GMainContext *context; OstreeRepoFinder *default_finders[4] = { NULL, }; g_autoptr(OstreeRepoFinder) finder_config = NULL; g_autoptr(OstreeRepoFinder) finder_mount = NULL; + g_autoptr(OstreeRepoFinder) finder_avahi = NULL; g_return_if_fail (OSTREE_IS_REPO (self)); g_return_if_fail (is_valid_collection_ref_array (refs)); @@ -4102,21 +4109,43 @@ ostree_repo_find_remotes_async (OstreeRepo *self, task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, ostree_repo_find_remotes_async); - context = g_main_context_get_thread_default (); - /* Are we using #OstreeRepoFinders provided by the user, or the defaults? */ if (finders == NULL) { +#ifdef HAVE_AVAHI + GMainContext *context = g_main_context_get_thread_default (); + g_autoptr(GError) local_error = NULL; +#endif /* HAVE_AVAHI */ + finder_config = OSTREE_REPO_FINDER (ostree_repo_finder_config_new ()); finder_mount = OSTREE_REPO_FINDER (ostree_repo_finder_mount_new (NULL)); +#ifdef HAVE_AVAHI + finder_avahi = OSTREE_REPO_FINDER (ostree_repo_finder_avahi_new (context)); +#endif /* HAVE_AVAHI */ default_finders[0] = finder_config; default_finders[1] = finder_mount; + default_finders[2] = finder_avahi; finders = default_finders; + +#ifdef HAVE_AVAHI + ostree_repo_finder_avahi_start (OSTREE_REPO_FINDER_AVAHI (finder_avahi), + &local_error); + + if (local_error != NULL) + { + g_warning ("Avahi finder failed; removing it: %s", local_error->message); + default_finders[2] = NULL; + g_clear_object (&finder_avahi); + } +#endif /* HAVE_AVAHI */ } - data = find_remotes_data_new (refs, options, progress); + /* We need to keep a pointer to the default Avahi finder so we can stop it + * again after the operation, which happens implicitly by dropping the final + * ref. */ + data = find_remotes_data_new (refs, options, progress, finder_avahi); g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) find_remotes_data_free); /* Asynchronously resolve all possible remotes for the given refs. */ diff --git a/src/libostree/ostree.h b/src/libostree/ostree.h index 0fe2a23e..0f727384 100644 --- a/src/libostree/ostree.h +++ b/src/libostree/ostree.h @@ -38,6 +38,7 @@ #ifdef OSTREE_ENABLE_EXPERIMENTAL_API #include #include +#include #include #include #endif /* OSTREE_ENABLE_EXPERIMENTAL_API */ diff --git a/tests/.gitignore b/tests/.gitignore index 5ece7ea1..9bec67a3 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -15,6 +15,7 @@ test-mutable-tree test-ot-opt-utils test-ot-tool-util test-ot-unix-utils +test-repo-finder-avahi test-repo-finder-config test-repo-finder-mount test-rollsum-cli diff --git a/tests/test-repo-finder-avahi.c b/tests/test-repo-finder-avahi.c new file mode 100644 index 00000000..b2fddf70 --- /dev/null +++ b/tests/test-repo-finder-avahi.c @@ -0,0 +1,228 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2017 Endless Mobile, Inc. + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: + * - Philip Withnall + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "ostree-autocleanups.h" +#include "ostree-repo-finder.h" +#include "ostree-repo-finder-avahi.h" +#include "ostree-repo-finder-avahi-private.h" + +/* FIXME: Upstream this */ +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AvahiStringList, avahi_string_list_free) + +/* Test the object constructor works at a basic level. */ +static void +test_repo_finder_avahi_init (void) +{ + g_autoptr(OstreeRepoFinderAvahi) finder = NULL; + g_autoptr(GMainContext) context = NULL; + + /* Default main context. */ + finder = ostree_repo_finder_avahi_new (NULL); + g_clear_object (&finder); + + /* Explicit main context. */ + context = g_main_context_new (); + finder = ostree_repo_finder_avahi_new (context); + g_clear_object (&finder); +} + +/* Test parsing valid and invalid TXT records. */ +static void +test_repo_finder_avahi_txt_records_parse (void) +{ + struct + { + const guint8 *txt; + gsize txt_len; + const gchar *expected_key; /* (nullable) to indicate parse failure */ + const guint8 *expected_value; /* (nullable) to allow for valueless keys */ + gsize expected_value_len; + } + vectors[] = + { + { (const guint8 *) "", 0, NULL, NULL, 0 }, + { (const guint8 *) "\x00", 1, NULL, NULL, 0 }, + { (const guint8 *) "\xff", 1, NULL, NULL, 0 }, + { (const guint8 *) "k\x00", 2, NULL, NULL, 0 }, + { (const guint8 *) "k\xff", 2, NULL, NULL, 0 }, + { (const guint8 *) "=", 1, NULL, NULL, 0 }, + { (const guint8 *) "=value", 6, NULL, NULL, 0 }, + { (const guint8 *) "k=v", 3, "k", (const guint8 *) "v", 1 }, + { (const guint8 *) "key=value", 9, "key", (const guint8 *) "value", 5 }, + { (const guint8 *) "k=v=", 4, "k", (const guint8 *) "v=", 2 }, + { (const guint8 *) "k=", 2, "k", (const guint8 *) "", 0 }, + { (const guint8 *) "k", 1, "k", NULL, 0 }, + { (const guint8 *) "k==", 3, "k", (const guint8 *) "=", 1 }, + { (const guint8 *) "k=\x00\x01\x02", 5, "k", (const guint8 *) "\x00\x01\x02", 3 }, + }; + gsize i; + + for (i = 0; i < G_N_ELEMENTS (vectors); i++) + { + g_autoptr(AvahiStringList) string_list = NULL; + g_autoptr(GHashTable) attributes = NULL; + + g_test_message ("Vector %" G_GSIZE_FORMAT, i); + + string_list = avahi_string_list_add_arbitrary (NULL, vectors[i].txt, vectors[i].txt_len); + + attributes = _ostree_txt_records_parse (string_list); + + if (vectors[i].expected_key != NULL) + { + GBytes *value; + g_autoptr(GBytes) expected_value = NULL; + + g_assert_true (g_hash_table_lookup_extended (attributes, + vectors[i].expected_key, + NULL, + (gpointer *) &value)); + g_assert_cmpuint (g_hash_table_size (attributes), ==, 1); + + if (vectors[i].expected_value != NULL) + { + g_assert_nonnull (value); + expected_value = g_bytes_new_static (vectors[i].expected_value, vectors[i].expected_value_len); + g_assert_true (g_bytes_equal (value, expected_value)); + } + else + { + g_assert_null (value); + } + } + else + { + g_assert_cmpuint (g_hash_table_size (attributes), ==, 0); + } + } +} + +/* Test that the first value for a set of duplicate records is returned. + * See RFC 6763, §6.4. */ +static void +test_repo_finder_avahi_txt_records_duplicates (void) +{ + g_autoptr(AvahiStringList) string_list = NULL; + g_autoptr(GHashTable) attributes = NULL; + GBytes *value; + g_autoptr(GBytes) expected_value = NULL; + + /* Reverse the list before using it, as they are built in reverse order. + * (See the #AvahiStringList documentation.) */ + string_list = avahi_string_list_new ("k=value1", "k=value2", "k=value3", NULL); + string_list = avahi_string_list_reverse (string_list); + attributes = _ostree_txt_records_parse (string_list); + + g_assert_cmpuint (g_hash_table_size (attributes), ==, 1); + value = g_hash_table_lookup (attributes, "k"); + g_assert_nonnull (value); + + expected_value = g_bytes_new_static ("value1", strlen ("value1")); + g_assert_true (g_bytes_equal (value, expected_value)); +} + +/* Test that keys are parsed and looked up case insensitively. + * See RFC 6763, §6.4. */ +static void +test_repo_finder_avahi_txt_records_case_sensitivity (void) +{ + g_autoptr(AvahiStringList) string_list = NULL; + g_autoptr(GHashTable) attributes = NULL; + GBytes *value1, *value2; + g_autoptr(GBytes) expected_value1 = NULL, expected_value2 = NULL; + + /* Reverse the list before using it, as they are built in reverse order. + * (See the #AvahiStringList documentation.) */ + string_list = avahi_string_list_new ("k=value1", + "K=value2", + "KeY2=v", + NULL); + string_list = avahi_string_list_reverse (string_list); + attributes = _ostree_txt_records_parse (string_list); + + g_assert_cmpuint (g_hash_table_size (attributes), ==, 2); + + value1 = g_hash_table_lookup (attributes, "k"); + g_assert_nonnull (value1); + expected_value1 = g_bytes_new_static ("value1", strlen ("value1")); + g_assert_true (g_bytes_equal (value1, expected_value1)); + + g_assert_null (g_hash_table_lookup (attributes, "K")); + + value2 = g_hash_table_lookup (attributes, "key2"); + g_assert_nonnull (value2); + expected_value2 = g_bytes_new_static ("v", 1); + g_assert_true (g_bytes_equal (value2, expected_value2)); + + g_assert_null (g_hash_table_lookup (attributes, "KeY2")); +} + +/* Test that keys which have an empty value can be distinguished from those + * which have no value. See RFC 6763, §6.4. */ +static void +test_repo_finder_avahi_txt_records_empty_and_missing (void) +{ + g_autoptr(AvahiStringList) string_list = NULL; + g_autoptr(GHashTable) attributes = NULL; + GBytes *value1, *value2; + g_autoptr(GBytes) expected_value1 = NULL; + + string_list = avahi_string_list_new ("empty=", + "missing", + NULL); + attributes = _ostree_txt_records_parse (string_list); + + g_assert_cmpuint (g_hash_table_size (attributes), ==, 2); + + value1 = g_hash_table_lookup (attributes, "empty"); + g_assert_nonnull (value1); + expected_value1 = g_bytes_new_static ("", 0); + g_assert_true (g_bytes_equal (value1, expected_value1)); + + g_assert_true (g_hash_table_lookup_extended (attributes, "missing", NULL, (gpointer *) &value2)); + g_assert_null (value2); +} + +int main (int argc, char **argv) +{ + setlocale (LC_ALL, ""); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/repo-finder-avahi/init", test_repo_finder_avahi_init); + g_test_add_func ("/repo-finder-avahi/txt-records/parse", test_repo_finder_avahi_txt_records_parse); + g_test_add_func ("/repo-finder-avahi/txt-records/duplicates", test_repo_finder_avahi_txt_records_duplicates); + g_test_add_func ("/repo-finder-avahi/txt-records/case-sensitivity", test_repo_finder_avahi_txt_records_case_sensitivity); + g_test_add_func ("/repo-finder-avahi/txt-records/empty-and-missing", test_repo_finder_avahi_txt_records_empty_and_missing); + /* FIXME: Add tests for service processing, probably by splitting the + * code in OstreeRepoFinderAvahi around found_services. */ + + return g_test_run(); +}