/* * Copyright © 2017 Endless Mobile, Inc. * * SPDX-License-Identifier: LGPL-2.0+ * * 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 #include "ostree-autocleanups.h" #include "ostree-remote-private.h" #include "ostree-repo-private.h" #include "ostree-repo-finder.h" #include "ostree-repo-finder-mount.h" /** * SECTION:ostree-repo-finder-mount * @title: OstreeRepoFinderMount * @short_description: Finds remote repositories from ref names by looking at * mounted removable volumes * @stability: Unstable * @include: libostree/ostree-repo-finder-mount.h * * #OstreeRepoFinderMount is an implementation of #OstreeRepoFinder which looks * refs up in well-known locations on any mounted removable volumes. * * For each mounted removable volume, the directory `.ostree/repos.d` will be * enumerated, and all OSTree repositories below it will be searched, in lexical * order, for the requested #OstreeCollectionRefs. The names of the directories * below `.ostree/repos.d` are irrelevant, apart from their lexical ordering. * The directories `.ostree/repo`, `ostree/repo` and `var/lib/flatpak/repo` * will be searched after the others, if they exist. * Non-removable volumes are ignored. * * 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/heads` and * `refs/mirrors` in the repository on the removable volume. * * Symlinks are followed when listing the repositories, so a volume might * contain a single OSTree at some arbitrary path, with a symlink from * `.ostree/repos.d`. Any symlink which points outside the volume’s file * system will be ignored. Repositories are deduplicated in the results. * * The volume monitor used to find mounted volumes can be overridden by setting * #OstreeRepoFinderMount:monitor. By default, g_volume_monitor_get() is used. * * Since: 2018.6 */ typedef GList/**/ ObjectList; static void object_list_free (ObjectList *list) { g_list_free_full (list, g_object_unref); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (ObjectList, object_list_free) static void ostree_repo_finder_mount_iface_init (OstreeRepoFinderInterface *iface); struct _OstreeRepoFinderMount { GObject parent_instance; GVolumeMonitor *monitor; /* owned */ }; G_DEFINE_TYPE_WITH_CODE (OstreeRepoFinderMount, ostree_repo_finder_mount, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (OSTREE_TYPE_REPO_FINDER, ostree_repo_finder_mount_iface_init)) typedef struct { gchar *uri; OstreeRemote *keyring_remote; /* (owned) */ } UriAndKeyring; static void uri_and_keyring_free (UriAndKeyring *data) { g_free (data->uri); ostree_remote_unref (data->keyring_remote); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (UriAndKeyring, uri_and_keyring_free) static UriAndKeyring * uri_and_keyring_new (const gchar *uri, OstreeRemote *keyring_remote) { g_autoptr(UriAndKeyring) data = NULL; data = g_new0 (UriAndKeyring, 1); data->uri = g_strdup (uri); data->keyring_remote = ostree_remote_ref (keyring_remote); 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_remote->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_remote->keyring, _b->keyring_remote->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_remote->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); } 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); } typedef struct { char *ordering_name; /* (owned) */ OstreeRepo *repo; /* (owned) */ GHashTable *refs; /* (owned) (element-type OstreeCollectionRef utf8) */ } RepoAndRefs; static void repo_and_refs_clear (RepoAndRefs *data) { g_hash_table_unref (data->refs); g_object_unref (data->repo); g_free (data->ordering_name); } static gint repo_and_refs_compare (gconstpointer a, gconstpointer b) { const RepoAndRefs *_a = a; const RepoAndRefs *_b = b; return strcmp (_a->ordering_name, _b->ordering_name); } /* Check whether the repo at @dfd/@path is within the given mount, is not equal * to the @parent_repo, and can be opened. If so, return it as @out_repo and * all its collection–refs as @out_refs, to be added into the results. */ static gboolean scan_repo (int dfd, const char *path, const char *mount_name, const struct stat *mount_root_stbuf, OstreeRepo *parent_repo, OstreeRepo **out_repo, GHashTable **out_refs, GCancellable *cancellable, GError **error) { g_autoptr(GError) local_error = NULL; g_autoptr(OstreeRepo) repo = ostree_repo_open_at (dfd, path, cancellable, &local_error); if (repo == NULL) { g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as it could not be opened: %s", path, mount_name, local_error->message); g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } int repo_dfd = ostree_repo_get_dfd (repo); struct stat stbuf; if (!glnx_fstat (repo_dfd, &stbuf, &local_error)) { g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as querying its info failed: %s", path, mount_name, local_error->message); g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } /* Check the resolved repository path is below the mount point. Do not * allow ref symlinks to point somewhere outside of the mounted volume. */ if (stbuf.st_dev != mount_root_stbuf->st_dev) { g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as it’s on a different file system from the mount", path, mount_name); return glnx_throw (error, "Repository is on a different file system from the mount"); } /* Exclude repositories which resolve to @parent_repo. */ if (stbuf.st_dev == parent_repo->device && stbuf.st_ino == parent_repo->inode) { g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as it is the same as the one we are resolving", path, mount_name); return glnx_throw (error, "Repository is the same as the one we are resolving"); } /* List the repo’s refs and return them. */ g_autoptr(GHashTable) repo_refs = NULL; /* (element-type OstreeCollectionRef utf8) */ if (!ostree_repo_list_collection_refs (repo, NULL, &repo_refs, OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_REMOTES, cancellable, &local_error)) { g_debug ("Ignoring repository ‘%s’ on mount ‘%s’ as its refs could not be listed: %s", path, mount_name, local_error->message); g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } if (out_repo != NULL) *out_repo = g_steal_pointer (&repo); if (out_refs != NULL) *out_refs = g_steal_pointer (&repo_refs); return TRUE; } static void scan_and_add_repo (int dfd, const char *path, gboolean sortable, const char *mount_name, const struct stat *mount_root_stbuf, OstreeRepo *parent_repo, GArray *inout_repos_refs, GCancellable *cancellable) { g_autoptr(GHashTable) repo_refs = NULL; g_autoptr(OstreeRepo) repo = NULL; if (scan_repo (dfd, path, mount_name, mount_root_stbuf, parent_repo, &repo, &repo_refs, cancellable, NULL)) { RepoAndRefs val = { sortable ? g_strdup (path) : NULL, g_steal_pointer (&repo), g_steal_pointer (&repo_refs) }; g_array_append_val (inout_repos_refs, val); g_debug ("%s: Adding repo ‘%s’ on mount ‘%s’ (%ssortable)", G_STRFUNC, path, mount_name, sortable ? "" : "not "); } } static void ostree_repo_finder_mount_resolve_async (OstreeRepoFinder *finder, const OstreeCollectionRef * const *refs, OstreeRepo *parent_repo, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (finder); g_autoptr(GTask) task = NULL; g_autoptr(ObjectList) mounts = NULL; g_autoptr(GPtrArray) results = NULL; /* (element-type OstreeRepoFinderResult) */ GList *l; const gint priority = 50; /* arbitrarily chosen */ task = g_task_new (finder, cancellable, callback, user_data); g_task_set_source_tag (task, ostree_repo_finder_mount_resolve_async); mounts = g_volume_monitor_get_mounts (self->monitor); results = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_repo_finder_result_free); g_debug ("%s: Found %u mounts", G_STRFUNC, g_list_length (mounts)); for (l = mounts; l != NULL; l = l->next) { GMount *mount = G_MOUNT (l->data); g_autofree gchar *mount_name = NULL; g_autoptr(GFile) mount_root = NULL; g_autofree gchar *mount_root_path = NULL; glnx_autofd int mount_root_dfd = -1; struct stat mount_root_stbuf; glnx_autofd int repos_dfd = -1; gsize i; g_autoptr(GHashTable) repo_to_refs = NULL; /* (element-type UriAndKeyring GHashTable) */ GHashTable *supported_ref_to_checksum; /* (element-type OstreeCollectionRef utf8) */ GHashTableIter iter; g_autoptr(GError) local_error = NULL; mount_name = g_mount_get_name (mount); /* Check the mount’s general properties. */ if (g_mount_is_shadowed (mount)) { g_debug ("Ignoring mount ‘%s’ as it’s shadowed.", mount_name); continue; } mount_root = g_mount_get_root (mount); mount_root_path = g_file_get_path (mount_root); if (!glnx_opendirat (AT_FDCWD, mount_root_path, TRUE, &mount_root_dfd, &local_error)) { g_debug ("Ignoring mount ‘%s’ as ‘%s’ directory can’t be opened: %s", mount_name, mount_root_path, local_error->message); continue; } #if GLIB_CHECK_VERSION(2, 55, 0) G_GNUC_BEGIN_IGNORE_DEPRECATIONS /* remove once GLIB_VERSION_MAX_ALLOWED ≥ 2.56 */ g_autoptr(GUnixMountEntry) mount_entry = g_unix_mount_at (mount_root_path, NULL); if (mount_entry != NULL && (g_unix_is_system_fs_type (g_unix_mount_get_fs_type (mount_entry)) || g_unix_is_system_device_path (g_unix_mount_get_device_path (mount_entry)))) { g_debug ("Ignoring mount ‘%s’ as its file system type (%s) or device " "path (%s) indicate it’s a system mount.", mount_name, g_unix_mount_get_fs_type (mount_entry), g_unix_mount_get_device_path (mount_entry)); continue; } G_GNUC_END_IGNORE_DEPRECATIONS #endif /* GLib 2.56.0 */ /* stat() the mount root so we can later check whether the resolved * repositories for individual refs are on the same device (to avoid the * symlinks for them pointing outside the mount root). */ if (!glnx_fstat (mount_root_dfd, &mount_root_stbuf, &local_error)) { g_debug ("Ignoring mount ‘%s’ as querying info of ‘%s’ failed: %s", mount_name, mount_root_path, local_error->message); continue; } /* Check if it contains a .ostree/repos.d directory. If not, move on and * try the other well-known subdirectories. */ if (!glnx_opendirat (mount_root_dfd, ".ostree/repos.d", TRUE, &repos_dfd, NULL)) repos_dfd = -1; /* List all the repositories in the repos.d directory. */ /* (element-type GHashTable (element-type OstreeCollectionRef utf8)) */ g_autoptr(GArray) repos_refs = g_array_new (FALSE, TRUE, sizeof (RepoAndRefs)); g_array_set_clear_func (repos_refs, (GDestroyNotify) repo_and_refs_clear); GLnxDirFdIterator repos_iter; if (repos_dfd >= 0 && !glnx_dirfd_iterator_init_at (repos_dfd, ".", TRUE, &repos_iter, &local_error)) { g_debug ("Error iterating over ‘%s/.ostree/repos.d’ directory in mount ‘%s’: %s", mount_root_path, mount_name, local_error->message); g_clear_error (&local_error); /* don’t skip this mount as there’s still the ostree/repo directory to try */ } else if (repos_dfd >= 0) { while (TRUE) { struct dirent *repo_dent; if (!glnx_dirfd_iterator_next_dent (&repos_iter, &repo_dent, cancellable, &local_error)) { g_debug ("Error iterating over ‘%s/.ostree/repos.d’ directory in mount ‘%s’: %s", mount_root_path, mount_name, local_error->message); g_clear_error (&local_error); /* don’t skip this mount as there’s still the ostree/repo directory to try */ break; } if (repo_dent == NULL) break; /* Grab the set of collection–refs from the repo if we can open it. */ scan_and_add_repo (repos_dfd, repo_dent->d_name, TRUE, mount_name, &mount_root_stbuf, parent_repo, repos_refs, cancellable); } } /* Sort the repos lexically. */ g_array_sort (repos_refs, repo_and_refs_compare); /* Also check the well-known special-case directories in the mount. * Add them after sorting, so they’re always last. * NOTE: If you change these, update the man page. */ const gchar * const well_known_repos[] = { ".ostree/repo", "ostree/repo", "var/lib/flatpak/repo", }; for (i = 0; i < G_N_ELEMENTS (well_known_repos); i++) scan_and_add_repo (mount_root_dfd, well_known_repos[i], FALSE, mount_name, &mount_root_stbuf, parent_repo, repos_refs, cancellable); /* Check whether a subdirectory exists for any of the @refs we’re looking * for. If so, and it’s a symbolic link, dereference it so multiple links * to the same repository (containing multiple refs) are coalesced. * Otherwise, include it as a result by itself. */ 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); for (i = 0; refs[i] != NULL; i++) { const OstreeCollectionRef *ref = refs[i]; g_autofree gchar *resolved_repo_uri = NULL; g_autoptr(UriAndKeyring) resolved_repo = NULL; for (gsize j = 0; j < repos_refs->len; j++) { const RepoAndRefs *repo_and_refs = &g_array_index (repos_refs, RepoAndRefs, j); OstreeRepo *repo = repo_and_refs->repo; GHashTable *repo_refs = repo_and_refs->refs; g_autofree char *repo_path = g_file_get_path (ostree_repo_get_path (repo)); g_autoptr(OstreeRemote) keyring_remote = NULL; const gchar *checksum = g_hash_table_lookup (repo_refs, ref); if (checksum == NULL) { g_debug ("Ignoring repository ‘%s’ when looking for ref (%s, %s) on mount ‘%s’ as it doesn’t contain the ref.", repo_path, ref->collection_id, ref->ref_name, mount_name); g_clear_error (&local_error); continue; } /* Finally, look up the GPG keyring for this ref. */ keyring_remote = ostree_repo_resolve_keyring_for_collection (parent_repo, ref->collection_id, cancellable, &local_error); if (keyring_remote == NULL) { g_debug ("Ignoring repository ‘%s’ when looking for ref (%s, %s) on mount ‘%s’ due to missing keyring: %s", repo_path, ref->collection_id, ref->ref_name, mount_name, local_error->message); g_clear_error (&local_error); continue; } /* There is a valid repo at (or pointed to by) * $mount_root/.ostree/repos.d/$something. * Add it to the results, keyed by the canonicalised repository URI * to deduplicate the results. */ g_autofree char *canonical_repo_path = realpath (repo_path, NULL); resolved_repo_uri = g_strconcat ("file://", canonical_repo_path, NULL); g_debug ("Resolved ref (%s, %s) on mount ‘%s’ to repo URI ‘%s’ with keyring ‘%s’ from remote ‘%s’.", ref->collection_id, ref->ref_name, mount_name, resolved_repo_uri, keyring_remote->keyring, keyring_remote->name); resolved_repo = uri_and_keyring_new (resolved_repo_uri, keyring_remote); 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 */); } g_hash_table_insert (supported_ref_to_checksum, (gpointer) ref, g_strdup (checksum)); /* We’ve found a result for this collection–ref. No point in checking * the other repos on the mount, since pulling in parallel from them won’t help. */ break; } } /* Aggregate the results. */ g_hash_table_iter_init (&iter, repo_to_refs); UriAndKeyring *repo; 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_dynamic (name, repo->keyring_remote->name); g_clear_pointer (&remote->keyring, g_free); remote->keyring = g_strdup (repo->keyring_remote->keyring); /* gpg-verify-summary is false since we use the unsigned summary file support. */ 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", FALSE); /* Set the timestamp in the #OstreeRepoFinderResult to 0 because * the code in ostree_repo_pull_from_remotes_async() will be able to * check it just as quickly as we can here; so don’t duplicate the * code. */ g_ptr_array_add (results, ostree_repo_finder_result_new (remote, finder, priority, supported_ref_to_checksum, NULL, 0)); } } g_ptr_array_sort (results, results_compare_cb); g_task_return_pointer (task, g_steal_pointer (&results), (GDestroyNotify) g_ptr_array_unref); } static GPtrArray * ostree_repo_finder_mount_resolve_finish (OstreeRepoFinder *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, self), NULL); return g_task_propagate_pointer (G_TASK (result), error); } static void ostree_repo_finder_mount_init (OstreeRepoFinderMount *self) { /* Nothing to see here. */ } static void ostree_repo_finder_mount_constructed (GObject *object) { OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object); G_OBJECT_CLASS (ostree_repo_finder_mount_parent_class)->constructed (object); if (self->monitor == NULL) self->monitor = g_volume_monitor_get (); } typedef enum { PROP_MONITOR = 1, } OstreeRepoFinderMountProperty; static void ostree_repo_finder_mount_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object); switch ((OstreeRepoFinderMountProperty) property_id) { case PROP_MONITOR: g_value_set_object (value, self->monitor); break; default: g_assert_not_reached (); } } static void ostree_repo_finder_mount_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object); switch ((OstreeRepoFinderMountProperty) property_id) { case PROP_MONITOR: /* Construct-only. */ g_assert (self->monitor == NULL); self->monitor = g_value_dup_object (value); break; default: g_assert_not_reached (); } } static void ostree_repo_finder_mount_dispose (GObject *object) { OstreeRepoFinderMount *self = OSTREE_REPO_FINDER_MOUNT (object); g_clear_object (&self->monitor); G_OBJECT_CLASS (ostree_repo_finder_mount_parent_class)->dispose (object); } static void ostree_repo_finder_mount_class_init (OstreeRepoFinderMountClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = ostree_repo_finder_mount_get_property; object_class->set_property = ostree_repo_finder_mount_set_property; object_class->constructed = ostree_repo_finder_mount_constructed; object_class->dispose = ostree_repo_finder_mount_dispose; /** * OstreeRepoFinderMount:monitor: * * Volume monitor to use to look up mounted volumes when queried. * * Since: 2018.6 */ g_object_class_install_property (object_class, PROP_MONITOR, g_param_spec_object ("monitor", "Volume Monitor", "Volume monitor to use " "to look up mounted " "volumes when queried.", G_TYPE_VOLUME_MONITOR, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); } static void ostree_repo_finder_mount_iface_init (OstreeRepoFinderInterface *iface) { iface->resolve_async = ostree_repo_finder_mount_resolve_async; iface->resolve_finish = ostree_repo_finder_mount_resolve_finish; } /** * ostree_repo_finder_mount_new: * @monitor: (nullable) (transfer none): volume monitor to use, or %NULL to use * the system default * * Create a new #OstreeRepoFinderMount, using the given @monitor to look up * volumes. If @monitor is %NULL, the monitor from g_volume_monitor_get() will * be used. * * Returns: (transfer full): a new #OstreeRepoFinderMount * Since: 2018.6 */ OstreeRepoFinderMount * ostree_repo_finder_mount_new (GVolumeMonitor *monitor) { g_return_val_if_fail (monitor == NULL || G_IS_VOLUME_MONITOR (monitor), NULL); return g_object_new (OSTREE_TYPE_REPO_FINDER_MOUNT, "monitor", monitor, NULL); }