Files
ostree/src/libostree/ostree-metalink.c
T
Colin Walters 9020fe2547 Change OstreeFetcher to be dirfd-relative
This is a noticeable cleanup, and fixes another big user of GFile* in
performance/security sensitive codepaths.

I'm specifically making this change because the static deltas code was
leaking temporary files, and cleaning that up nicely would be best if
we were fd relative.
2015-01-14 22:12:08 -05:00

739 lines
23 KiB
C

/* -*- 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 <gio/gfiledescriptorbased.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;
char *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;
struct stat stbuf;
int parent_dfd = _ostree_fetcher_get_dfd (self->metalink->fetcher);
gs_unref_object GInputStream *instream = NULL;
gs_free char *result = NULL;
GChecksum *checksum = NULL;
result = _ostree_fetcher_request_uri_with_partial_finish ((OstreeFetcher*)src, res, &local_error);
if (!result)
goto out;
if (!ot_openat_read_stream (parent_dfd, result, FALSE,
&instream, NULL, &local_error))
goto out;
if (fstat (g_file_descriptor_based_get_fd ((GFileDescriptorBased*)instream), &stbuf) != 0)
{
gs_set_error_from_errno (&local_error, errno);
goto out;
}
if (stbuf.st_size != 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, stbuf.st_size);
goto out;
}
if (self->verification_sha512)
{
const char *actual;
checksum = g_checksum_new (G_CHECKSUM_SHA512);
if (!ot_gio_splice_update_checksum (NULL, instream, checksum,
g_task_get_cancellable (task),
&local_error))
goto out;
actual = g_checksum_get_string (checksum);
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;
}
}
else if (self->verification_sha256)
{
const char *actual;
checksum = g_checksum_new (G_CHECKSUM_SHA256);
if (!ot_gio_splice_update_checksum (NULL, instream, checksum,
g_task_get_cancellable (task),
&local_error))
goto out;
actual = g_checksum_get_string (checksum);
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 (checksum)
g_checksum_free (checksum);
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 = result;
result = NULL; /* Transfer ownership */
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,
OSTREE_FETCHER_DEFAULT_PRIORITY,
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
ostree_metalink_request_unref (gpointer data)
{
OstreeMetalinkRequest *request = data;
g_object_unref (request->metalink);
g_free (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
};
typedef struct
{
SoupURI **out_target_uri;
char **out_data;
gboolean success;
GError **error;
GMainLoop *loop;
} FetchMetalinkSyncData;
static gboolean
ostree_metalink_request_finish (OstreeMetalink *self,
GAsyncResult *result,
SoupURI **out_target_uri,
char **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_strdup (request->result);
return TRUE;
}
else
return FALSE;
}
static void
on_metalink_fetched (GObject *src,
GAsyncResult *result,
gpointer user_data)
{
FetchMetalinkSyncData *data = user_data;
data->success = ostree_metalink_request_finish ((OstreeMetalink*)src,
result,
data->out_target_uri,
data->out_data,
data->error);
g_main_loop_quit (data->loop);
}
static gboolean
on_metalink_bytes_read (OstreeMetalinkRequest *self,
OstreeMetalinkRequest *request,
FetchMetalinkSyncData *sync_data,
GBytes *bytes,
GError **error)
{
gsize len;
const guint8 *data = g_bytes_get_data (bytes, &len);
if (!g_markup_parse_context_parse (self->parser, (const char*)data, len, error))
return FALSE;
if (!start_target_request_phase (self, error))
return FALSE;
return TRUE;
}
gboolean
_ostree_metalink_request_sync (OstreeMetalink *self,
GMainLoop *loop,
SoupURI **out_target_uri,
char **out_data,
SoupURI **fetching_sync_uri,
GCancellable *cancellable,
GError **error)
{
OstreeMetalinkRequest *request = g_new0 (OstreeMetalinkRequest, 1);
FetchMetalinkSyncData data = { 0, };
GTask *task = g_task_new (self, cancellable, on_metalink_fetched, &data);
GBytes *out_contents = NULL;
data.out_target_uri = out_target_uri;
data.out_data = out_data;
data.loop = loop;
data.error = error;
*fetching_sync_uri = _ostree_metalink_get_uri (self);
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);
if (! _ostree_fetcher_request_uri_to_membuf (self->fetcher,
self->uri,
FALSE,
FALSE,
&out_contents,
loop,
self->max_size,
cancellable,
error))
goto out;
if (! on_metalink_bytes_read (request, request, &data, out_contents, error))
goto out;
g_main_loop_run (data.loop);
out:
return data.success;
}
SoupURI *
_ostree_metalink_get_uri (OstreeMetalink *self)
{
return self->uri;
}