/*
* 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, see .
*
* Authors:
* - Philip Withnall
*/
#include "config.h"
#include
#include
#include
#include
#include
#include
#include "ostree-autocleanups.h"
#include "ostree-remote-private.h"
#include "ostree-repo-finder-mount.h"
#include "ostree-repo-finder.h"
#include "ostree-repo-private.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);
}