summaryrefslogtreecommitdiff
path: root/src/libostree/ostree-metalink.c
diff options
context:
space:
mode:
authorColin Walters <walters@verbum.org>2014-07-31 18:50:19 -0400
committerColin Walters <walters@verbum.org>2014-09-03 13:21:52 -0400
commitf8f5da219edd2279322bba916879fd53c2b65350 (patch)
tree2bb77d53e3568e70a164112f4b63246737fba45a /src/libostree/ostree-metalink.c
parent3571418557e3a69d5e86dc464499a59df3d38315 (diff)
downloadostree-f8f5da219edd2279322bba916879fd53c2b65350.tar.gz
Add repository "summary" file and metalink support
For Fedora and potentially other distributions which use globally distributed mirrors, metalink is a popular solution to redirect clients to a dynamic set of mirrors. In order to make metalink work though, it needs *one* file which can be checksummed. (Well, potentially we could explode all refs into the metalink.xml, but that would be a lot more invasive, and a bit weird as we'd end up checksumming the checksum file). This commit adds a new command: $ ostree summary -u To regenerate the summary file. Can only be run by one process at a time. After that's done, the metalink can be generated based on it, and the client fetch code will parse and load it. https://bugzilla.gnome.org/show_bug.cgi?id=729585
Diffstat (limited to 'src/libostree/ostree-metalink.c')
-rw-r--r--src/libostree/ostree-metalink.c710
1 files changed, 710 insertions, 0 deletions
diff --git a/src/libostree/ostree-metalink.c b/src/libostree/ostree-metalink.c
new file mode 100644
index 00000000..00cfb2c8
--- /dev/null
+++ b/src/libostree/ostree-metalink.c
@@ -0,0 +1,710 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2014 Colin Walters <walters@verbum.org>
+ *
+ * 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.
+ */
+
+#include "config.h"
+
+#include "ostree-metalink.h"
+
+#include "otutil.h"
+#include "libgsystem.h"
+
+typedef enum {
+ OSTREE_METALINK_STATE_INITIAL,
+ OSTREE_METALINK_STATE_METALINK,
+ OSTREE_METALINK_STATE_FILES,
+ OSTREE_METALINK_STATE_FILE,
+ OSTREE_METALINK_STATE_SIZE,
+ OSTREE_METALINK_STATE_VERIFICATION,
+ OSTREE_METALINK_STATE_HASH,
+ OSTREE_METALINK_STATE_RESOURCES,
+ OSTREE_METALINK_STATE_URL,
+
+ OSTREE_METALINK_STATE_PASSTHROUGH /* Ignoring unknown elements */
+} OstreeMetalinkState;
+
+struct OstreeMetalink
+{
+ GObject parent_instance;
+
+ SoupURI *uri;
+
+ OstreeFetcher *fetcher;
+ char *requested_file;
+ guint64 max_size;
+};
+
+G_DEFINE_TYPE (OstreeMetalink, _ostree_metalink, G_TYPE_OBJECT)
+
+typedef struct
+{
+ OstreeMetalink *metalink;
+
+ GTask *task;
+ GMarkupParseContext *parser;
+
+ guint passthrough_depth;
+ OstreeMetalinkState passthrough_previous;
+
+ guint found_a_file_element : 1;
+ guint found_our_file_element : 1;
+ guint verification_known : 1;
+
+ GChecksumType in_verification_type;
+
+ guint64 size;
+ char *verification_sha256;
+ char *verification_sha512;
+
+ GFile *result;
+
+ char *last_metalink_error;
+ guint current_url_index;
+ GPtrArray *urls;
+
+ OstreeMetalinkState state;
+} OstreeMetalinkRequest;
+
+static void
+state_transition (OstreeMetalinkRequest *self,
+ OstreeMetalinkState new_state)
+{
+ g_assert (self->state != new_state);
+ self->state = new_state;
+}
+
+static void
+unknown_element (OstreeMetalinkRequest *self,
+ const char *element_name,
+ GError **error)
+{
+ state_transition (self, OSTREE_METALINK_STATE_PASSTHROUGH);
+ g_assert (self->passthrough_depth == 0);
+}
+
+static void
+metalink_parser_start (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error)
+{
+ GTask *task = user_data;
+ OstreeMetalinkRequest *self = g_task_get_task_data (task);
+
+ switch (self->state)
+ {
+ case OSTREE_METALINK_STATE_INITIAL:
+ if (strcmp (element_name, "metalink") == 0)
+ state_transition (self, OSTREE_METALINK_STATE_METALINK);
+ else
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_METALINK:
+ if (strcmp (element_name, "files") == 0)
+ state_transition (self, OSTREE_METALINK_STATE_FILES);
+ else
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_FILES:
+ /* If we've already processed a <file> element we're OK with, just
+ * ignore the others.
+ */
+ if (self->urls->len > 0)
+ {
+ state_transition (self, OSTREE_METALINK_STATE_PASSTHROUGH);
+ }
+ else if (strcmp (element_name, "file") == 0)
+ {
+ const char *file_name;
+
+ if (!g_markup_collect_attributes (element_name,
+ attribute_names,
+ attribute_values,
+ error,
+ G_MARKUP_COLLECT_STRING,
+ "name",
+ &file_name,
+ G_MARKUP_COLLECT_INVALID))
+ goto out;
+
+ self->found_a_file_element = TRUE;
+
+ if (strcmp (file_name, self->metalink->requested_file) != 0)
+ {
+ state_transition (self, OSTREE_METALINK_STATE_PASSTHROUGH);
+ g_assert (self->passthrough_depth == 0);
+ }
+ else
+ {
+ self->found_our_file_element = TRUE;
+ state_transition (self, OSTREE_METALINK_STATE_FILE);
+ }
+ }
+ else
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_FILE:
+ if (strcmp (element_name, "size") == 0)
+ state_transition (self, OSTREE_METALINK_STATE_SIZE);
+ else if (strcmp (element_name, "verification") == 0)
+ state_transition (self, OSTREE_METALINK_STATE_VERIFICATION);
+ else if (strcmp (element_name, "resources") == 0)
+ state_transition (self, OSTREE_METALINK_STATE_RESOURCES);
+ else
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_SIZE:
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_VERIFICATION:
+ if (strcmp (element_name, "hash") == 0)
+ {
+ char *verification_type_str = NULL;
+
+ state_transition (self, OSTREE_METALINK_STATE_HASH);
+ if (!g_markup_collect_attributes (element_name,
+ attribute_names,
+ attribute_values,
+ error,
+ G_MARKUP_COLLECT_STRING,
+ "type",
+ &verification_type_str,
+ G_MARKUP_COLLECT_INVALID))
+ goto out;
+
+ /* Only accept sha256/sha512 */
+ self->verification_known = TRUE;
+ if (strcmp (verification_type_str, "sha256") == 0)
+ self->in_verification_type = G_CHECKSUM_SHA256;
+ else if (strcmp (verification_type_str, "sha512") == 0)
+ self->in_verification_type = G_CHECKSUM_SHA512;
+ else
+ self->verification_known = FALSE;
+ }
+ else
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_HASH:
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_RESOURCES:
+ if (self->size == 0)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "No <size> element found or it is zero");
+ goto out;
+ }
+ if (!self->verification_known)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "No <verification> element with known <hash type=> found");
+ goto out;
+ }
+
+ if (strcmp (element_name, "url") == 0)
+ {
+ const char *protocol;
+
+ if (!g_markup_collect_attributes (element_name,
+ attribute_names,
+ attribute_values,
+ error,
+ G_MARKUP_COLLECT_STRING,
+ "protocol",
+ &protocol,
+ G_MARKUP_COLLECT_STRING,
+ "type",
+ NULL,
+ G_MARKUP_COLLECT_STRING,
+ "location",
+ NULL,
+ G_MARKUP_COLLECT_STRING,
+ "preference",
+ NULL,
+ G_MARKUP_COLLECT_INVALID))
+ goto out;
+
+ /* Ignore non-HTTP resources */
+ if (!(strcmp (protocol, "http") == 0 || strcmp (protocol, "https") == 0))
+ state_transition (self, OSTREE_METALINK_STATE_PASSTHROUGH);
+ else
+ state_transition (self, OSTREE_METALINK_STATE_URL);
+ }
+ else
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_URL:
+ unknown_element (self, element_name, error);
+ break;
+ case OSTREE_METALINK_STATE_PASSTHROUGH:
+ self->passthrough_depth++;
+ break;
+ }
+
+ out:
+ return;
+}
+
+static void
+metalink_parser_end (GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data,
+ GError **error)
+{
+ GTask *task = user_data;
+ OstreeMetalinkRequest *self = g_task_get_task_data (task);
+
+ switch (self->state)
+ {
+ case OSTREE_METALINK_STATE_INITIAL:
+ break;
+ case OSTREE_METALINK_STATE_METALINK:
+ state_transition (self, OSTREE_METALINK_STATE_INITIAL);
+ break;
+ case OSTREE_METALINK_STATE_FILES:
+ state_transition (self, OSTREE_METALINK_STATE_METALINK);
+ break;
+ case OSTREE_METALINK_STATE_FILE:
+ state_transition (self, OSTREE_METALINK_STATE_FILES);
+ break;
+ case OSTREE_METALINK_STATE_SIZE:
+ case OSTREE_METALINK_STATE_VERIFICATION:
+ case OSTREE_METALINK_STATE_RESOURCES:
+ state_transition (self, OSTREE_METALINK_STATE_FILE);
+ break;
+ case OSTREE_METALINK_STATE_HASH:
+ state_transition (self, OSTREE_METALINK_STATE_VERIFICATION);
+ break;
+ case OSTREE_METALINK_STATE_URL:
+ state_transition (self, OSTREE_METALINK_STATE_RESOURCES);
+ break;
+ case OSTREE_METALINK_STATE_PASSTHROUGH:
+ g_assert_cmpint (self->passthrough_depth, >, 0);
+ self->passthrough_depth--;
+ if (self->passthrough_depth == 0)
+ state_transition (self, self->passthrough_previous);
+ break;
+ }
+}
+
+static void
+metalink_parser_text (GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error)
+{
+ GTask *task = user_data;
+ OstreeMetalinkRequest *self = g_task_get_task_data (task);
+
+ switch (self->state)
+ {
+ case OSTREE_METALINK_STATE_INITIAL:
+ break;
+ case OSTREE_METALINK_STATE_METALINK:
+ break;
+ case OSTREE_METALINK_STATE_FILES:
+ break;
+ case OSTREE_METALINK_STATE_FILE:
+ break;
+ case OSTREE_METALINK_STATE_SIZE:
+ {
+ gs_free char *duped = g_strndup (text, text_len);
+ self->size = g_ascii_strtoull (duped, NULL, 10);
+ }
+ break;
+ case OSTREE_METALINK_STATE_VERIFICATION:
+ break;
+ case OSTREE_METALINK_STATE_HASH:
+ if (self->verification_known)
+ {
+ switch (self->in_verification_type)
+ {
+ case G_CHECKSUM_SHA256:
+ g_free (self->verification_sha256);
+ self->verification_sha256 = g_strndup (text, text_len);
+ break;
+ case G_CHECKSUM_SHA512:
+ g_free (self->verification_sha512);
+ self->verification_sha512 = g_strndup (text, text_len);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ }
+ break;
+ case OSTREE_METALINK_STATE_RESOURCES:
+ break;
+ case OSTREE_METALINK_STATE_URL:
+ {
+ gs_free char *uri_text = g_strndup (text, text_len);
+ SoupURI *uri = soup_uri_new (uri_text);
+ if (uri != NULL)
+ g_ptr_array_add (self->urls, uri);
+ }
+ break;
+ case OSTREE_METALINK_STATE_PASSTHROUGH:
+ break;
+ }
+
+}
+
+static void
+_ostree_metalink_finalize (GObject *object)
+{
+ OstreeMetalink *self;
+
+ self = OSTREE_METALINK (object);
+
+ g_object_unref (self->fetcher);
+ g_free (self->requested_file);
+ soup_uri_free (self->uri);
+
+ G_OBJECT_CLASS (_ostree_metalink_parent_class)->finalize (object);
+}
+
+static void
+_ostree_metalink_class_init (OstreeMetalinkClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = _ostree_metalink_finalize;
+}
+
+static void
+_ostree_metalink_init (OstreeMetalink *self)
+{
+}
+
+OstreeMetalink *
+_ostree_metalink_new (OstreeFetcher *fetcher,
+ const char *requested_file,
+ guint64 max_size,
+ SoupURI *uri)
+{
+ OstreeMetalink *self = (OstreeMetalink*)g_object_new (OSTREE_TYPE_METALINK, NULL);
+
+ self->fetcher = g_object_ref (fetcher);
+ self->requested_file = g_strdup (requested_file);
+ self->max_size = max_size;
+ self->uri = soup_uri_copy (uri);
+
+ return self;
+}
+
+static void
+try_next_url (OstreeMetalinkRequest *self);
+
+static gboolean
+valid_hex_checksum (const char *s, gsize expected_len)
+{
+ gsize len = strspn (s, "01234567890abcdef");
+
+ return len == expected_len && s[len] == '\0';
+}
+
+static void
+on_fetched_url (GObject *src,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GTask *task = user_data;
+ OstreeMetalinkRequest *self = g_task_get_task_data (task);
+ GError *local_error = NULL;
+ gs_unref_object GFile *result = NULL;
+ gs_unref_object GFileInfo *finfo = NULL;
+
+ result = _ostree_fetcher_request_uri_with_partial_finish ((OstreeFetcher*)src, res, &local_error);
+ if (!result)
+ goto out;
+
+ finfo = g_file_query_info (result, OSTREE_GIO_FAST_QUERYINFO, 0,
+ g_task_get_cancellable (task), &local_error);
+ if (!finfo)
+ goto out;
+
+ if (g_file_info_get_size (finfo) != self->size)
+ {
+ g_set_error (&local_error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Expected size is %" G_GUINT64_FORMAT " bytes but content is %" G_GUINT64_FORMAT " bytes",
+ self->size, g_file_info_get_size (finfo));
+ goto out;
+ }
+
+ if (self->verification_sha512)
+ {
+ gs_free char *actual = ot_checksum_file (result, G_CHECKSUM_SHA512,
+ g_task_get_cancellable (task),
+ &local_error);
+
+ if (!actual)
+ goto out;
+
+ if (strcmp (self->verification_sha512, actual) != 0)
+ {
+ g_set_error (&local_error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Expected checksum is %s but actual is %s",
+ self->verification_sha512, actual);
+ goto out;
+ }
+ }
+
+ if (self->verification_sha256)
+ {
+ gs_free char *actual = ot_checksum_file (result, G_CHECKSUM_SHA256,
+ g_task_get_cancellable (task),
+ &local_error);
+
+ if (!actual)
+ goto out;
+
+ if (strcmp (self->verification_sha256, actual) != 0)
+ {
+ g_set_error (&local_error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Expected checksum is %s but actual is %s",
+ self->verification_sha256, actual);
+ goto out;
+ }
+ }
+
+ out:
+ if (local_error)
+ {
+ g_free (self->last_metalink_error);
+ self->last_metalink_error = g_strdup (local_error->message);
+ g_clear_error (&local_error);
+
+ /* And here we iterate on the next one if we hit an error */
+ self->current_url_index++;
+ try_next_url (self);
+ }
+ else
+ {
+ self->result = g_object_ref (result);
+ g_task_return_boolean (self->task, TRUE);
+ }
+}
+
+static void
+try_next_url (OstreeMetalinkRequest *self)
+{
+ if (self->current_url_index >= self->urls->len)
+ {
+ g_task_return_new_error (self->task, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Exhausted %u metalink targets, last error: %s",
+ self->urls->len, self->last_metalink_error);
+ }
+ else
+ {
+ SoupURI *next = self->urls->pdata[self->current_url_index];
+
+ _ostree_fetcher_request_uri_with_partial_async (self->metalink->fetcher, next,
+ self->metalink->max_size,
+ g_task_get_cancellable (self->task),
+ on_fetched_url, self->task);
+ }
+}
+
+static gboolean
+start_target_request_phase (OstreeMetalinkRequest *self,
+ GError **error)
+{
+ gboolean ret = FALSE;
+
+ if (!self->found_a_file_element)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "No <file> element found");
+ goto out;
+ }
+
+ if (!self->found_our_file_element)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "No <file name='%s'> found", self->metalink->requested_file);
+ goto out;
+ }
+
+ if (!(self->verification_sha256 || self->verification_sha512))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "No <verification> hash for sha256 or sha512 found");
+ goto out;
+ }
+
+ if (self->verification_sha256 && !valid_hex_checksum (self->verification_sha256, 64))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Invalid hash digest for sha256");
+ goto out;
+ }
+
+ if (self->verification_sha512 && !valid_hex_checksum (self->verification_sha512, 128))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Invalid hash digest for sha512");
+ goto out;
+ }
+
+ if (self->urls->len == 0)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "No <url method='http'> elements found");
+ goto out;
+ }
+
+ try_next_url (self);
+
+ ret = TRUE;
+ out:
+ return ret;
+}
+
+static void
+on_metalink_bytes_read (GObject *src,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *local_error = NULL;
+ GTask *task = user_data;
+ OstreeMetalinkRequest *self = g_task_get_task_data (task);
+ gs_unref_bytes GBytes *bytes = NULL;
+ gsize len;
+ const guint8 *data;
+
+ bytes = g_input_stream_read_bytes_finish ((GInputStream*)src,
+ result, &local_error);
+ if (!bytes)
+ goto out;
+
+ data = g_bytes_get_data (bytes, &len);
+
+ if (len == 0)
+ {
+ if (!start_target_request_phase (self, &local_error))
+ goto out;
+ }
+ else
+ {
+ if (!g_markup_parse_context_parse (self->parser, (const char*)data, len, &local_error))
+ goto out;
+
+ g_input_stream_read_bytes_async ((GInputStream*)src, 8192, G_PRIORITY_DEFAULT,
+ g_task_get_cancellable (task),
+ on_metalink_bytes_read, task);
+ }
+
+ out:
+ if (local_error)
+ g_task_return_error (task, local_error);
+}
+
+static void
+on_retrieved_metalink (GObject *src,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *local_error = NULL;
+ GTask *task = user_data;
+ gs_unref_object GInputStream *metalink_stream = NULL;
+
+ metalink_stream = _ostree_fetcher_stream_uri_finish ((OstreeFetcher*)src, result, &local_error);
+ if (!metalink_stream)
+ goto out;
+
+ g_input_stream_read_bytes_async (metalink_stream, 8192, G_PRIORITY_DEFAULT,
+ g_task_get_cancellable (task),
+ on_metalink_bytes_read, task);
+
+ out:
+ if (local_error)
+ g_task_return_error (task, local_error);
+}
+
+static void
+ostree_metalink_request_unref (gpointer data)
+{
+ OstreeMetalinkRequest *request = data;
+ g_object_unref (request->metalink);
+ g_clear_object (&request->result);
+ g_free (request->last_metalink_error);
+ g_ptr_array_unref (request->urls);
+ g_free (request);
+}
+
+static const GMarkupParser metalink_parser = {
+ metalink_parser_start,
+ metalink_parser_end,
+ metalink_parser_text,
+ NULL,
+ NULL
+};
+
+void
+_ostree_metalink_request_async (OstreeMetalink *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask *task = g_task_new (self, cancellable, callback, user_data);
+ OstreeMetalinkRequest *request = g_new0 (OstreeMetalinkRequest, 1);
+
+ request->metalink = g_object_ref (self);
+ request->urls = g_ptr_array_new_with_free_func ((GDestroyNotify) soup_uri_free);
+ request->task = task; /* Unowned */
+
+ request->parser = g_markup_parse_context_new (&metalink_parser, G_MARKUP_PREFIX_ERROR_POSITION, task, NULL);
+
+ g_task_set_task_data (task, request, ostree_metalink_request_unref);
+ _ostree_fetcher_stream_uri_async (self->fetcher, self->uri,
+ self->max_size, cancellable,
+ on_retrieved_metalink, task);
+}
+
+gboolean
+_ostree_metalink_request_finish (OstreeMetalink *self,
+ GAsyncResult *result,
+ SoupURI **out_target_uri,
+ GFile **out_data,
+ GError **error)
+{
+ OstreeMetalinkRequest *request;
+
+ g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
+
+ request = g_task_get_task_data ((GTask*)result);
+
+ if (g_task_propagate_boolean ((GTask*)result, error))
+ {
+ g_assert_cmpint (request->current_url_index, <, request->urls->len);
+ *out_target_uri = request->urls->pdata[request->current_url_index];
+ *out_data = g_object_ref (request->result);
+ return TRUE;
+ }
+ else
+ return FALSE;
+}
+
+SoupURI *
+_ostree_metalink_get_uri (OstreeMetalink *self)
+{
+ return self->uri;
+}