ostree/src/libostree/ostree-repo-finder-mount.c

667 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <withnall@endlessm.com>
*/
#include "config.h"
#include <gio/gio.h>
#include <glib.h>
#include <glib-object.h>
#include <libglnx.h>
#include <stdlib.h>
#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 directory `ostree/repo` will be searched after the others, if it exists.
* 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 volumes 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: 2017.8
*/
typedef GList/*<owned GObject>*/ 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;
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 its 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 collectionrefs 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 its on a different file system from the mount",
path, mount_name);
g_propagate_error (error, g_steal_pointer (&local_error));
return FALSE;
}
/* 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);
g_propagate_error (error, g_steal_pointer (&local_error));
return FALSE;
}
/* List the repos 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 (%ssortable)",
G_STRFUNC, path, 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_fd_close int mount_root_dfd = -1;
struct stat mount_root_stbuf;
glnx_fd_close 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;
UriAndKeyring *repo;
g_autoptr(GError) local_error = NULL;
mount_name = g_mount_get_name (mount);
/* Check the mounts general properties. */
if (g_mount_is_shadowed (mount))
{
g_debug ("Ignoring mount %s as its 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 cant be opened: %s",
mount_name, mount_root_path, local_error->message);
continue;
}
/* 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);
/* dont skip this mount as theres 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);
/* dont skip this mount as theres still the ostree/repo directory to try */
break;
}
if (repo_dent == NULL)
break;
/* Grab the set of collectionrefs 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 .ostree/repo and ostree/repo directories in the mount,
* as well-known special cases. Add them after sorting, so theyre always
* last. */
scan_and_add_repo (mount_root_dfd, ".ostree/repo", FALSE,
mount_name, &mount_root_stbuf,
parent_repo, repos_refs, cancellable);
scan_and_add_repo (mount_root_dfd, "ostree/repo", FALSE,
mount_name, &mount_root_stbuf,
parent_repo, repos_refs, cancellable);
/* Check whether a subdirectory exists for any of the @refs were looking
* for. If so, and its 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_autofree gchar *keyring = 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));
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 doesnt 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 = ostree_repo_resolve_keyring_for_collection (parent_repo, ref->collection_id,
cancellable, &local_error);
if (keyring == 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.",
ref->collection_id, ref->ref_name, mount_name, resolved_repo_uri, keyring);
resolved_repo = uri_and_keyring_new (resolved_repo_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 */);
}
g_hash_table_insert (supported_ref_to_checksum, (gpointer) ref, g_strdup (checksum));
/* Weve found a result for this collectionref. No point in checking
* the other repos on the mount, since pulling in parallel from them wont help. */
break;
}
}
/* 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);
/* 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 dont duplicate the
* code. */
g_ptr_array_add (results, ostree_repo_finder_result_new (remote, finder, priority, supported_ref_to_checksum, 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: 2017.8
*/
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: 2017.8
*/
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);
}