diff options
author | Patrick <pgriffis@igalia.com> | 2023-01-07 18:48:28 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-07 18:48:28 -0600 |
commit | e110bf7c7fc28ede5bde59a8a28cfe8b163595e4 (patch) | |
tree | 63283a96075325e86b37c3d57a7aaa1c9d2eb4e0 /common/flatpak-utils-http.c | |
parent | b61a6d836c30d446c707f50585f7a91a8ae1857d (diff) | |
parent | 523cedc27509779e7e815806e53361d5fe7e0bd4 (diff) | |
download | flatpak-appstreamcli-compose.tar.gz |
Merge branch 'main' into appstreamcli-composeappstreamcli-compose
Diffstat (limited to 'common/flatpak-utils-http.c')
-rw-r--r-- | common/flatpak-utils-http.c | 1528 |
1 files changed, 986 insertions, 542 deletions
diff --git a/common/flatpak-utils-http.c b/common/flatpak-utils-http.c index 6f2421ba..e180345a 100644 --- a/common/flatpak-utils-http.c +++ b/common/flatpak-utils-http.c @@ -1,4 +1,4 @@ -/* +/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s: * Copyright © 2018 Red Hat, Inc * * This program is free software; you can redistribute it and/or @@ -18,21 +18,64 @@ * Alexander Larsson <alexl@redhat.com> */ +#include "config.h" + +#include <gio/gio.h> +#include <glib-unix.h> #include "flatpak-utils-http-private.h" +#include "flatpak-uri-private.h" #include "flatpak-oci-registry-private.h" #include <gio/gunixoutputstream.h> -#include <libsoup/soup.h> #include "libglnx.h" #include <sys/types.h> #include <sys/xattr.h> +#if defined(HAVE_CURL) + +#include <curl/curl.h> + +/* These macros came from 7.43.0, but we want to check + * for versions a bit earlier than that (to work on CentOS 7), + * so define them here if we're using an older version. + */ +#ifndef CURL_VERSION_BITS +#define CURL_VERSION_BITS(x,y,z) ((x)<<16|(y)<<8|z) +#endif +#ifndef CURL_AT_LEAST_VERSION +#define CURL_AT_LEAST_VERSION(x,y,z) (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS(x, y, z)) +#endif + +#elif defined(HAVE_SOUP) + +#include <libsoup/soup.h> + +#if !defined(SOUP_AUTOCLEANUPS_H) && !defined(__SOUP_AUTOCLEANUPS_H__) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupSession, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupMessage, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupRequest, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupRequestHTTP, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupURI, soup_uri_free) +#endif + +#else + +# error "No HTTP backend enabled" + +#endif + + +#define FLATPAK_HTTP_TIMEOUT_SECS 60 + /* copied from libostree */ #define DEFAULT_N_NETWORK_RETRIES 5 G_DEFINE_QUARK (flatpak_http_error, flatpak_http_error) +/* Information about the cache status of a file. + Encoded in an xattr on the cached file, or a file on the side if xattrs don't work. +*/ typedef struct { char *uri; @@ -46,290 +89,516 @@ typedef struct GMainContext *context; gboolean done; GError *error; - gboolean store_compressed; + + /* Input args */ + + FlatpakHTTPFlags flags; + const char *auth; + const char *token; + FlatpakLoadUriProgress progress; + GCancellable *cancellable; + gpointer user_data; + CacheHttpData *cache_data; + + /* Output from the request, set even on http server errors */ + + guint64 downloaded_bytes; + int status; + char *hdr_content_type; + char *hdr_www_authenticate; + char *hdr_etag; + char *hdr_last_modified; + char *hdr_cache_control; + char *hdr_expires; + char *hdr_content_encoding; + + /* Data destination */ GOutputStream *out; /*or */ GString *content; /* or */ GLnxTmpfile *out_tmpfile; int out_tmpfile_parent_dfd; - guint64 downloaded_bytes; + /* Used during operation */ + char buffer[16 * 1024]; - FlatpakLoadUriProgress progress; - GCancellable *cancellable; - gpointer user_data; guint64 last_progress_time; - CacheHttpData *cache_data; - char **content_type_out; + gboolean store_compressed; + } LoadUriData; -#define CACHE_HTTP_XATTR "user.flatpak.http" -#define CACHE_HTTP_SUFFIX ".flatpak.http" -#define CACHE_HTTP_TYPE "(sstt)" +static void +clear_load_uri_data_headers (LoadUriData *data) +{ + g_clear_pointer (&data->hdr_content_type, g_free); + g_clear_pointer (&data->hdr_www_authenticate, g_free); + g_clear_pointer (&data->hdr_etag, g_free); + g_clear_pointer (&data->hdr_last_modified, g_free); + g_clear_pointer (&data->hdr_last_modified, g_free); + g_clear_pointer (&data->hdr_cache_control, g_free); + g_clear_pointer (&data->hdr_expires, g_free); + g_clear_pointer (&data->hdr_content_encoding, g_free); +} +/* Reset between requests retries */ static void -clear_cache_http_data (CacheHttpData *data, - gboolean clear_uri) +reset_load_uri_data (LoadUriData *data) { - if (clear_uri) - g_clear_pointer (&data->uri, g_free); - g_clear_pointer (&data->etag, g_free); - data->last_modified = 0; - data->expires = 0; + g_clear_error (&data->error); + data->status = 0; + data->downloaded_bytes = 0; + if (data->content) + g_string_set_size (data->content, 0); + + clear_load_uri_data_headers (data); + + if (data->out_tmpfile) + { + glnx_tmpfile_clear (data->out_tmpfile); + g_clear_pointer (&data->out, g_object_unref); + } + + /* Reset the progress */ + if (data->progress) + data->progress (0, data->user_data); } +/* Free allocated data at end of full repeated download */ static void -free_cache_http_data (CacheHttpData *data) +clear_load_uri_data (LoadUriData *data) { - clear_cache_http_data (data, TRUE); - g_free (data); + if (data->content) + { + g_string_free (data->content, TRUE); + data->content = NULL; + } + + g_clear_error (&data->error); + + clear_load_uri_data_headers (data); } -G_DEFINE_AUTOPTR_CLEANUP_FUNC (CacheHttpData, free_cache_http_data) +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(LoadUriData, clear_load_uri_data) -static GBytes * -serialize_cache_http_data (CacheHttpData * data) +static gboolean +check_http_status (guint status_code, + GError **error) { - g_autoptr(GVariant) cache_variant = NULL; + GQuark domain; + int code; - cache_variant = g_variant_ref_sink (g_variant_new (CACHE_HTTP_TYPE, - data->uri, - data->etag ? data->etag : "", - data->last_modified, - data->expires)); - if (G_BYTE_ORDER != G_BIG_ENDIAN) + if (status_code >= 200 && status_code < 300) + return TRUE; + + switch (status_code) { - g_autoptr(GVariant) tmp_variant = cache_variant; - cache_variant = g_variant_byteswap (tmp_variant); + case 304: /* Not Modified */ + domain = FLATPAK_HTTP_ERROR; + code = FLATPAK_HTTP_ERROR_NOT_CHANGED; + break; + + case 401: /* Unauthorized */ + domain = FLATPAK_HTTP_ERROR; + code = FLATPAK_HTTP_ERROR_UNAUTHORIZED; + break; + + case 403: /* Forbidden */ + case 404: /* Not found */ + case 410: /* Gone */ + domain = G_IO_ERROR; + code = G_IO_ERROR_NOT_FOUND; + break; + + case 408: /* Request Timeout */ + domain = G_IO_ERROR; + code = G_IO_ERROR_TIMED_OUT; + break; + + case 500: /* Internal Server Error */ + /* The server did return something, but it was useless to us, so that’s basically equivalent to not returning */ + domain = G_IO_ERROR; + code = G_IO_ERROR_HOST_UNREACHABLE; + break; + + default: + domain = G_IO_ERROR; + code = G_IO_ERROR_FAILED; } - return g_variant_get_data_as_bytes (cache_variant); + g_set_error (error, domain, code, + "Server returned status %u", + status_code); + return FALSE; } +#if defined(HAVE_CURL) + +/************************************************************************ + * Curl implementation * + ************************************************************************/ + +typedef struct curl_slist auto_curl_slist; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (auto_curl_slist, curl_slist_free_all) + +struct FlatpakHttpSession { + CURL *curl; + GMutex lock; +}; + static void -deserialize_cache_http_data (CacheHttpData *data, - GBytes *bytes) +check_header(char **value_out, + const char *header, + char *buffer, + size_t realsize) { - g_autoptr(GVariant) cache_variant = NULL; + size_t hlen = strlen (header); - cache_variant = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE (CACHE_HTTP_TYPE), - bytes, - FALSE)); - if (G_BYTE_ORDER != G_BIG_ENDIAN) + if (realsize < hlen + 1) + return; + + if (!g_ascii_strncasecmp(buffer, header, hlen) == 0 || + buffer[hlen] != ':') + return; + + buffer += hlen + 1; + realsize -= hlen + 1; + + while (realsize > 0 && g_ascii_isspace (*buffer)) { - g_autoptr(GVariant) tmp_variant = cache_variant; - cache_variant = g_variant_byteswap (tmp_variant); + buffer++; + realsize--; } - g_variant_get (cache_variant, - CACHE_HTTP_TYPE, - &data->uri, - &data->etag, - &data->last_modified, - &data->expires); + while (realsize > 0 && g_ascii_isspace (buffer[realsize-1])) + realsize--; + + g_free (*value_out); /* Use the last header */ + *value_out = g_strndup (buffer, realsize); } -static CacheHttpData * -load_cache_http_data (int dfd, - char *name, - gboolean *no_xattr, - GCancellable *cancellable, - GError **error) +static size_t +_header_cb (char *buffer, + size_t size, + size_t nitems, + void *userdata) { - g_autoptr(CacheHttpData) data = NULL; - g_autoptr(GBytes) cache_bytes = glnx_lgetxattrat (dfd, name, - CACHE_HTTP_XATTR, - error); - if (cache_bytes == NULL) - { - if (errno == ENOTSUP) - { - g_autofree char *cache_file = NULL; - glnx_autofd int fd = -1; + size_t realsize = size * nitems; + LoadUriData *data = (LoadUriData *)userdata; - g_clear_error (error); - *no_xattr = TRUE; + check_header(&data->hdr_content_type, "content-type", buffer, realsize); + check_header(&data->hdr_www_authenticate, "WWW-Authenticate", buffer, realsize); - cache_file = g_strconcat (name, CACHE_HTTP_SUFFIX, NULL); + check_header(&data->hdr_etag, "ETag", buffer, realsize); + check_header(&data->hdr_last_modified, "Last-Modified", buffer, realsize); + check_header(&data->hdr_cache_control, "Cache-Control", buffer, realsize); + check_header(&data->hdr_expires, "Expires", buffer, realsize); + check_header(&data->hdr_content_encoding, "Content-Encoding", buffer, realsize); - if (!glnx_openat_rdonly (dfd, cache_file, FALSE, - &fd, error)) - return FALSE; + return realsize; +} - cache_bytes = glnx_fd_readall_bytes (fd, cancellable, error); - if (!cache_bytes) - return NULL; +static size_t +_write_cb (void *content_data, + size_t size, + size_t nmemb, + void *userp) +{ + size_t realsize = size * nmemb; + LoadUriData *data = (LoadUriData *)userp; + gsize n_written = 0; + + /* If first write to tmpfile, initiate if needed */ + if (data->content == NULL && data->out == NULL && + data->out_tmpfile != NULL) + { + g_autoptr(GOutputStream) out = NULL; + g_autoptr(GError) tmp_error = NULL; + + if (!glnx_open_tmpfile_linkable_at (data->out_tmpfile_parent_dfd, ".", + O_WRONLY, data->out_tmpfile, + &tmp_error)) + { + g_warning ("Failed to open http tmpfile: %s\n", tmp_error->message); + return 0; /* This short read will make curl report an error */ } - else if (errno == ENOENT || errno == ENODATA) + + out = g_unix_output_stream_new (data->out_tmpfile->fd, FALSE); + if (data->store_compressed && + g_strcmp0 (data->hdr_content_encoding, "gzip") != 0) { - g_clear_error (error); - return g_new0 (CacheHttpData, 1); + g_autoptr(GZlibCompressor) compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, -1); + data->out = g_converter_output_stream_new (out, G_CONVERTER (compressor)); } else { - return NULL; + data->out = g_steal_pointer (&out); } } + if (data->content) + { + g_string_append_len (data->content, content_data, realsize); + n_written = realsize; + } + else if (data->out) + { + /* We ignore the error here, but reporting a short read will make curl report the error */ + g_output_stream_write_all (data->out, content_data, realsize, + &n_written, NULL, NULL); + } - data = g_new0 (CacheHttpData, 1); - deserialize_cache_http_data (data, cache_bytes); - return g_steal_pointer (&data); + data->downloaded_bytes += realsize; + + if (g_get_monotonic_time () - data->last_progress_time > 1 * G_USEC_PER_SEC) + { + if (data->progress) + data->progress (data->downloaded_bytes, data->user_data); + data->last_progress_time = g_get_monotonic_time (); + } + + return realsize; } -static void -set_cache_http_data_from_headers (CacheHttpData *data, - SoupMessage *msg) +FlatpakHttpSession * +flatpak_create_http_session (const char *user_agent) { - const char *etag = soup_message_headers_get_one (msg->response_headers, "ETag"); - const char *last_modified = soup_message_headers_get_one (msg->response_headers, "Last-Modified"); - const char *cache_control = soup_message_headers_get_list (msg->response_headers, "Cache-Control"); - const char *expires = soup_message_headers_get_list (msg->response_headers, "Expires"); - gboolean expires_computed = FALSE; + FlatpakHttpSession *session = g_new0 (FlatpakHttpSession, 1); + CURLcode rc; + CURL *curl; - /* The original HTTP 1/1 specification only required sending the ETag header in a 304 - * response, and implied that a cache might need to save the old Cache-Control - * values. The updated RFC 7232 from 2014 requires sending Cache-Control, ETags, and - * Expire if they would have been sent in the original 200 response, and recommends - * sending Last-Modified for requests without an etag. Since sending these headers was - * apparently normal previously, for simplicity we assume the RFC 7232 behavior and start - * from scratch for a 304 response. + session->curl = curl = curl_easy_init(); + g_assert (session->curl != NULL); + + g_mutex_init (&session->lock); + + curl_easy_setopt (curl, CURLOPT_USERAGENT, user_agent); + rc = curl_easy_setopt (curl, CURLOPT_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS)); + g_assert_cmpint (rc, ==, CURLM_OK); + + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + /* Note: curl automatically respects the http_proxy env var */ + + if (g_getenv ("OSTREE_DEBUG_HTTP")) + curl_easy_setopt (curl, CURLOPT_VERBOSE, 1L); + + /* Picked the current version in F25 as of 20170127, since + * there are numerous HTTP/2 fixes since the original version in + * libcurl 7.43.0. */ - clear_cache_http_data (data, FALSE); +#if CURL_AT_LEAST_VERSION(7, 51, 0) + if ((curl_version_info (CURLVERSION_NOW))->features & CURL_VERSION_HTTP2) { + rc = curl_easy_setopt (curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); + g_assert_cmpint (rc, ==, CURLM_OK); + } +#endif + /* https://github.com/curl/curl/blob/curl-7_53_0/docs/examples/http2-download.c */ +#if (CURLPIPE_MULTIPLEX > 0) + /* wait for pipe connection to confirm */ + rc = curl_easy_setopt (curl, CURLOPT_PIPEWAIT, 1L); + g_assert_cmpint (rc, ==, CURLM_OK); +#endif - if (etag && *etag) + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _write_cb); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, _header_cb); + + curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, (long)FLATPAK_HTTP_TIMEOUT_SECS); + + return session; +} + +void +flatpak_http_session_free (FlatpakHttpSession* session) +{ + g_mutex_lock (&session->lock); + curl_easy_cleanup (session->curl); + g_mutex_unlock (&session->lock); + g_mutex_clear (&session->lock); + g_free (session); +} + +static void +set_error_from_curl (GError **error, + const char *uri, + CURLcode res) +{ + GQuark domain = G_IO_ERROR; + int code; + + switch (res) { - data->etag = g_strdup (etag); + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_COULDNT_RESOLVE_PROXY: + code = G_IO_ERROR_HOST_NOT_FOUND; + break; + case CURLE_OPERATION_TIMEDOUT: + code = G_IO_ERROR_TIMED_OUT; + break; + default: + code = G_IO_ERROR_FAILED; } - else if (last_modified && *last_modified) + + g_set_error (error, domain, code, + "While fetching %s: [%u] %s", uri, res, + curl_easy_strerror (res)); +} + +static gboolean +flatpak_download_http_uri_once (FlatpakHttpSession *session, + LoadUriData *data, + const char *uri, + GError **error) +{ + CURLcode res; + g_autofree char *auth_header = NULL; + g_autofree char *cache_header = NULL; + g_autoptr(auto_curl_slist) header_list = NULL; + g_autoptr(GMutexLocker) curl_lock = g_mutex_locker_new (&session->lock); + long response; + CURL *curl = session->curl; + + g_info ("Loading %s using curl", uri); + + curl_easy_setopt (curl, CURLOPT_URL, uri); + curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)data); + curl_easy_setopt (curl, CURLOPT_HEADERDATA, (void *)data); + + if (data->flags & FLATPAK_HTTP_FLAGS_HEAD) + curl_easy_setopt (curl, CURLOPT_NOBODY, 1L); + else + curl_easy_setopt (curl, CURLOPT_HTTPGET, 1L); + + if (data->flags & FLATPAK_HTTP_FLAGS_ACCEPT_OCI) + header_list = curl_slist_append (header_list, + "Accept: " FLATPAK_OCI_MEDIA_TYPE_IMAGE_MANIFEST ", " FLATPAK_DOCKER_MEDIA_TYPE_IMAGE_MANIFEST2 ", " FLATPAK_OCI_MEDIA_TYPE_IMAGE_INDEX); + + if (data->auth) + auth_header = g_strdup_printf ("Authorization: Basic %s", data->auth); + else if (data->token) + auth_header = g_strdup_printf ("Authorization: Bearer %s", data->token); + if (auth_header) + header_list = curl_slist_append (header_list, auth_header); + + if (data->cache_data) { - SoupDate *date = soup_date_new_from_string (last_modified); - if (date) + CacheHttpData *cache_data = data->cache_data; + + if (cache_data->etag && cache_data->etag[0]) + cache_header = g_strdup_printf ("If-None-Match: %s", cache_data->etag); + else if (cache_data->last_modified != 0) { - data->last_modified = soup_date_to_time_t (date); - soup_date_free (date); + g_autoptr(GDateTime) date = g_date_time_new_from_unix_utc (cache_data->last_modified); + g_autofree char *date_str = flatpak_format_http_date (date); + cache_header = g_strdup_printf ("If-Modified-Since: %s", date_str); } + if (cache_header) + header_list = curl_slist_append (header_list, cache_header); } - if (cache_control && *cache_control) + curl_easy_setopt (curl, CURLOPT_HTTPHEADER, header_list); + + if (data->flags & FLATPAK_HTTP_FLAGS_STORE_COMPRESSED) { - g_autoptr(GHashTable) params = soup_header_parse_param_list (cache_control); - GHashTableIter iter; - gpointer key, value; + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_HTTP_CONTENT_DECODING, 0L); + data->store_compressed = TRUE; + } + else + { + /* enable all supported built-in compressions */ + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + curl_easy_setopt(curl, CURLOPT_HTTP_CONTENT_DECODING, 1L); + data->store_compressed = FALSE; + } - g_hash_table_iter_init (&iter, params); - while (g_hash_table_iter_next (&iter, &key, &value)) - { - if (g_strcmp0 (key, "max-age") == 0) - { - char *end; + res = curl_easy_perform (session->curl); - char *max_age = value; - int max_age_sec = g_ascii_strtoll (max_age, &end, 10); - if (*max_age != '\0' && *end == '\0') - { - GTimeVal now; - g_get_current_time (&now); - data->expires = now.tv_sec + max_age_sec; - expires_computed = TRUE; - } - } - else if (g_strcmp0 (key, "no-cache") == 0) - { - data->expires = 0; - expires_computed = TRUE; - } - } - } + curl_easy_setopt (session->curl, CURLOPT_HTTPHEADER, NULL); /* Don't point to freed list */ - if (!expires_computed && expires && *expires) + if (res != CURLE_OK) { - SoupDate *date = soup_date_new_from_string (expires); - if (date) - { - data->expires = soup_date_to_time_t (date); - soup_date_free (date); - expires_computed = TRUE; - } + set_error_from_curl (error, uri, res); + + /* Make sure we clear the tmpfile stream we possible created during the request */ + if (data->out_tmpfile && data->out) + g_clear_pointer (&data->out, g_object_unref); + + return FALSE; } - if (!expires_computed) + if (data->out_tmpfile && data->out) { - /* If nothing implies an expires time, use 30 minutes. Browsers use - * 0.1 * (Date - Last-Modified), but it's clearly appropriate here, and - * better if server's send a value. - */ - GTimeVal now; - g_get_current_time (&now); - data->expires = now.tv_sec + 1800; + /* Flush the writes */ + if (!g_output_stream_close (data->out, data->cancellable, error)) + return FALSE; + + g_clear_pointer (&data->out, g_object_unref); } -} -static gboolean -save_cache_http_data_xattr (int fd, - GBytes *bytes, - GError **error) -{ - if (TEMP_FAILURE_RETRY (fsetxattr (fd, (char *) CACHE_HTTP_XATTR, - g_bytes_get_data (bytes, NULL), - g_bytes_get_size (bytes), - 0)) < 0) - return glnx_throw_errno_prefix (error, "fsetxattr"); + if (data->progress) + data->progress (data->downloaded_bytes, data->user_data); - return TRUE; -} + curl_easy_getinfo (session->curl, CURLINFO_RESPONSE_CODE, &response); -static gboolean -save_cache_http_data_fallback (int fd, - GBytes *bytes, - GError **error) -{ - if (glnx_loop_write (fd, - g_bytes_get_data (bytes, NULL), - g_bytes_get_size (bytes)) < 0) - return glnx_throw_errno_prefix (error, "write"); + data->status = response; + + if ((data->flags & FLATPAK_HTTP_FLAGS_NOCHECK_STATUS) == 0 && + !check_http_status (data->status, error)) + return FALSE; + + g_info ("Received %" G_GUINT64_FORMAT " bytes", data->downloaded_bytes); + + /* This is not really needed, but the auto-pointer confuses some compilers in the CI */ + g_clear_pointer (&curl_lock, g_mutex_locker_free); return TRUE; } +#endif /* HAVE_CURL */ + +#if defined(HAVE_SOUP) + +/************************************************************************ + * Soup implementation * + ***********************************************************************/ + static gboolean -save_cache_http_data_to_file (int dfd, - char *name, - GBytes *bytes, - gboolean no_xattr, - GCancellable *cancellable, - GError **error) +check_soup_transfer_error (SoupMessage *msg, GError **error) { - glnx_autofd int fd = -1; - g_autofree char *fallback_name = NULL; + GQuark domain = G_IO_ERROR; + int code; - if (!no_xattr) + if (!SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code)) + return TRUE; + + switch (msg->status_code) { - if (!glnx_openat_rdonly (dfd, name, FALSE, - &fd, error)) - return FALSE; + case SOUP_STATUS_CANCELLED: + code = G_IO_ERROR_CANCELLED; + break; - if (save_cache_http_data_xattr (fd, bytes, error)) - return TRUE; + case SOUP_STATUS_CANT_RESOLVE: + case SOUP_STATUS_CANT_CONNECT: + code = G_IO_ERROR_HOST_NOT_FOUND; + break; - if (errno == ENOTSUP) - g_clear_error (error); - else - return FALSE; - } + case SOUP_STATUS_IO_ERROR: + code = G_IO_ERROR_CONNECTION_CLOSED; + break; - fallback_name = g_strconcat (name, CACHE_HTTP_SUFFIX, NULL); - if (!glnx_file_replace_contents_at (dfd, fallback_name, - g_bytes_get_data (bytes, NULL), - g_bytes_get_size (bytes), - 0, - cancellable, - error)) - return FALSE; + default: + code = G_IO_ERROR_FAILED; + } - return TRUE; + g_set_error (error, domain, code, + "Error connecting to server: %s", + soup_status_get_phrase (msg->status_code)); + return FALSE; } +/* The soup input stream was closed */ static void stream_closed (GObject *source, GAsyncResult *res, gpointer user_data) { @@ -355,6 +624,7 @@ stream_closed (GObject *source, GAsyncResult *res, gpointer user_data) g_main_context_wakeup (data->context); } +/* Got some data from the soup input stream */ static void load_uri_read_cb (GObject *source, GAsyncResult *res, gpointer user_data) { @@ -392,6 +662,7 @@ load_uri_read_cb (GObject *source, GAsyncResult *res, gpointer user_data) } else { + g_assert (data->content != NULL); data->downloaded_bytes += nread; g_string_append_len (data->content, data->buffer, nread); } @@ -408,6 +679,7 @@ load_uri_read_cb (GObject *source, GAsyncResult *res, gpointer user_data) load_uri_read_cb, data); } +/* The http header part of the request is ready */ static void load_uri_callback (GObject *source_object, GAsyncResult *res, @@ -420,81 +692,37 @@ load_uri_callback (GObject *source_object, in = soup_request_send_finish (SOUP_REQUEST (request), res, &data->error); if (in == NULL) { - /* data->error has been set */ g_main_context_wakeup (data->context); return; } g_autoptr(SoupMessage) msg = soup_request_http_get_message ((SoupRequestHTTP *) request); - if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) + + if (!check_soup_transfer_error (msg, &data->error)) { - int code; - GQuark domain = G_IO_ERROR; + g_main_context_wakeup (data->context); + return; + } - switch (msg->status_code) - { - case 304: - if (data->cache_data) - set_cache_http_data_from_headers (data->cache_data, msg); - - domain = FLATPAK_HTTP_ERROR; - code = FLATPAK_HTTP_ERROR_NOT_CHANGED; - break; - - case 401: - domain = FLATPAK_HTTP_ERROR; - code = FLATPAK_HTTP_ERROR_UNAUTHORIZED; - break; - - case 403: - case 404: - case 410: - code = G_IO_ERROR_NOT_FOUND; - break; - - case 408: - code = G_IO_ERROR_TIMED_OUT; - break; - - case SOUP_STATUS_CANCELLED: - code = G_IO_ERROR_CANCELLED; - break; - - case SOUP_STATUS_CANT_RESOLVE: - case SOUP_STATUS_CANT_CONNECT: - code = G_IO_ERROR_HOST_NOT_FOUND; - break; - - case SOUP_STATUS_INTERNAL_SERVER_ERROR: - /* The server did return something, but it was useless to us, so that’s basically equivalent to not returning */ - code = G_IO_ERROR_HOST_UNREACHABLE; - break; - - case SOUP_STATUS_IO_ERROR: -#if !GLIB_CHECK_VERSION(2, 44, 0) - code = G_IO_ERROR_BROKEN_PIPE; -#else - code = G_IO_ERROR_CONNECTION_CLOSED; -#endif - break; + /* We correctly made a connection, although it may be a http failure like 404. + The status and headers are valid on return, even of a http failure though. */ - default: - code = G_IO_ERROR_FAILED; - } + data->status = msg->status_code; + data->hdr_content_type = g_strdup (soup_message_headers_get_content_type (msg->response_headers, NULL)); + data->hdr_www_authenticate = g_strdup (soup_message_headers_get_one (msg->response_headers, "WWW-Authenticate")); + data->hdr_etag = g_strdup (soup_message_headers_get_one (msg->response_headers, "ETag")); + data->hdr_last_modified = g_strdup (soup_message_headers_get_one (msg->response_headers, "Last-Modified")); + data->hdr_cache_control = g_strdup (soup_message_headers_get_list (msg->response_headers, "Cache-Control")); + data->hdr_expires = g_strdup (soup_message_headers_get_list (msg->response_headers, "Expires")); - data->error = g_error_new (domain, code, - "Server returned status %u: %s", - msg->status_code, - soup_status_get_phrase (msg->status_code)); + if ((data->flags & FLATPAK_HTTP_FLAGS_NOCHECK_STATUS) == 0 && + !check_http_status (data->status, &data->error)) + { g_main_context_wakeup (data->context); return; } - if (data->cache_data) - set_cache_http_data_from_headers (data->cache_data, msg); - - if (data->content_type_out) - *data->content_type_out = g_strdup (soup_message_headers_get_content_type (msg->response_headers, NULL)); + /* All is good, write the body to the destination */ if (data->out_tmpfile) { @@ -528,7 +756,7 @@ load_uri_callback (GObject *source_object, load_uri_read_cb, data); } -SoupSession * +static SoupSession * flatpak_create_soup_session (const char *user_agent) { SoupSession *soup_session; @@ -537,8 +765,8 @@ flatpak_create_soup_session (const char *user_agent) soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, user_agent, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, SOUP_SESSION_USE_THREAD_CONTEXT, TRUE, - SOUP_SESSION_TIMEOUT, 60, - SOUP_SESSION_IDLE_TIMEOUT, 60, + SOUP_SESSION_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS, + SOUP_SESSION_IDLE_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS, NULL); http_proxy = g_getenv ("http_proxy"); if (http_proxy) @@ -556,6 +784,102 @@ flatpak_create_soup_session (const char *user_agent) return soup_session; } +FlatpakHttpSession * +flatpak_create_http_session (const char *user_agent) +{ + return (FlatpakHttpSession *)flatpak_create_soup_session (user_agent); +} + +void +flatpak_http_session_free (FlatpakHttpSession* http_session) +{ + SoupSession *soup_session = (SoupSession *)http_session; + + g_object_unref (soup_session); +} + +static gboolean +flatpak_download_http_uri_once (FlatpakHttpSession *http_session, + LoadUriData *data, + const char *uri, + GError **error) +{ + SoupSession *soup_session = (SoupSession *)http_session; + g_autoptr(SoupRequestHTTP) request = NULL; + SoupMessage *m; + + g_info ("Loading %s using libsoup", uri); + + request = soup_session_request_http (soup_session, + (data->flags & FLATPAK_HTTP_FLAGS_HEAD) != 0 ? "HEAD" : "GET", + uri, error); + if (request == NULL) + return FALSE; + + m = soup_request_http_get_message (request); + + if (data->flags & FLATPAK_HTTP_FLAGS_ACCEPT_OCI) + soup_message_headers_replace (m->request_headers, "Accept", + FLATPAK_OCI_MEDIA_TYPE_IMAGE_MANIFEST ", " FLATPAK_DOCKER_MEDIA_TYPE_IMAGE_MANIFEST2 ", " FLATPAK_OCI_MEDIA_TYPE_IMAGE_INDEX); + + if (data->auth) + { + g_autofree char *basic_auth = g_strdup_printf ("Basic %s", data->auth); + soup_message_headers_replace (m->request_headers, "Authorization", basic_auth); + } + + if (data->token) + { + g_autofree char *bearer_token = g_strdup_printf ("Bearer %s", data->token); + soup_message_headers_replace (m->request_headers, "Authorization", bearer_token); + } + + if (data->cache_data) + { + CacheHttpData *cache_data = data->cache_data; + + if (cache_data->etag && cache_data->etag[0]) + soup_message_headers_replace (m->request_headers, "If-None-Match", cache_data->etag); + else if (cache_data->last_modified != 0) + { + g_autoptr(GDateTime) date = g_date_time_new_from_unix_utc (cache_data->last_modified); + g_autofree char *date_str = flatpak_format_http_date (date); + soup_message_headers_replace (m->request_headers, "If-Modified-Since", date_str); + } + } + + if (data->flags & FLATPAK_HTTP_FLAGS_STORE_COMPRESSED) + { + soup_session_remove_feature_by_type (soup_session, SOUP_TYPE_CONTENT_DECODER); + soup_message_headers_replace (m->request_headers, "Accept-Encoding", "gzip"); + data->store_compressed = TRUE; + } + else if (!soup_session_has_feature (soup_session, SOUP_TYPE_CONTENT_DECODER)) + { + soup_session_add_feature_by_type (soup_session, SOUP_TYPE_CONTENT_DECODER); + data->store_compressed = FALSE; + } + + soup_request_send_async (SOUP_REQUEST (request), + data->cancellable, + load_uri_callback, data); + + while (data->error == NULL && !data->done) + g_main_context_iteration (data->context, TRUE); + + if (data->error) + { + g_propagate_error (error, g_steal_pointer (&data->error)); + return FALSE; + } + + g_info ("Received %" G_GUINT64_FORMAT " bytes", data->downloaded_bytes); + + return TRUE; +} + +#endif /* HAVE_SOUP */ + /* Check whether a particular operation should be retried. This is entirely * based on how it failed (if at all) last time, and whether the operation has * some retries left. The retry count is set when the operation is first @@ -576,91 +900,103 @@ flatpak_http_should_retry_request (const GError *error, g_error_matches (error, G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND) || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_HOST_UNREACHABLE) || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT) || -#if !GLIB_CHECK_VERSION(2, 44, 0) - g_error_matches (error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE) || -#else g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED) || -#endif g_error_matches (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND) || g_error_matches (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_TEMPORARY_FAILURE)) { - g_debug ("Should retry request (remaining: %u retries), due to transient error: %s", - n_retries_remaining, error->message); + g_info ("Should retry request (remaining: %u retries), due to transient error: %s", + n_retries_remaining, error->message); return TRUE; } return FALSE; } -static GBytes * -flatpak_load_http_uri_once (SoupSession *soup_session, - const char *uri, - FlatpakHTTPFlags flags, - const char *token, - FlatpakLoadUriProgress progress, - gpointer user_data, - char **out_content_type, - GCancellable *cancellable, - GError **error) +GBytes * +flatpak_load_uri_full (FlatpakHttpSession *http_session, + const char *uri, + FlatpakHTTPFlags flags, + const char *auth, + const char *token, + FlatpakLoadUriProgress progress, + gpointer user_data, + int *out_status, + char **out_content_type, + char **out_www_authenticate, + GCancellable *cancellable, + GError **error) { - GBytes *bytes = NULL; - g_autoptr(GMainContext) context = NULL; - g_autoptr(SoupRequestHTTP) request = NULL; - g_autoptr(GString) content = g_string_new (""); - LoadUriData data = { NULL }; - SoupMessage *m; + g_auto(LoadUriData) data = { NULL }; + g_autoptr(GError) local_error = NULL; + guint n_retries_remaining = DEFAULT_N_NETWORK_RETRIES; + g_autoptr(GMainContextPopDefault) main_context = NULL; + gboolean success = FALSE; - g_debug ("Loading %s using libsoup", uri); + /* Ensure we handle file: uris the same independent of backend */ + if (g_ascii_strncasecmp (uri, "file:", 5) == 0) + { + g_autoptr(GFile) file = g_file_new_for_uri (uri); + gchar *contents; + gsize len; - context = g_main_context_ref_thread_default (); + if (!g_file_load_contents (file, cancellable, &contents, &len, NULL, error)) + return NULL; + + return g_bytes_new_take (g_steal_pointer (&contents), len); + } + + main_context = flatpak_main_context_new_default (); - data.context = context; - data.content = content; + data.context = main_context; data.progress = progress; - data.cancellable = cancellable; data.user_data = user_data; data.last_progress_time = g_get_monotonic_time (); - data.content_type_out = out_content_type; + data.cancellable = cancellable; + data.flags = flags; + data.auth = auth; + data.token = token; - request = soup_session_request_http (soup_session, "GET", - uri, error); - if (request == NULL) - return NULL; + data.content = g_string_new (""); - m = soup_request_http_get_message (request); + do + { + if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) + { + g_clear_error (&local_error); + reset_load_uri_data (&data); + } - if (flags & FLATPAK_HTTP_FLAGS_ACCEPT_OCI) - soup_message_headers_replace (m->request_headers, "Accept", - FLATPAK_OCI_MEDIA_TYPE_IMAGE_MANIFEST ", " FLATPAK_DOCKER_MEDIA_TYPE_IMAGE_MANIFEST2 ", " FLATPAK_OCI_MEDIA_TYPE_IMAGE_INDEX); + success = flatpak_download_http_uri_once (http_session, &data, uri, &local_error); + if (success) + break; - if (token) - { - g_autofree char *bearer_token = g_strdup_printf ("Bearer %s", token); - soup_message_headers_replace (m->request_headers, "Authorization", bearer_token); + g_assert (local_error != NULL); } + while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); - soup_request_send_async (SOUP_REQUEST (request), - cancellable, - load_uri_callback, &data); + if (success) + { + if (out_content_type) + *out_content_type = g_steal_pointer (&data.hdr_content_type); - while (data.error == NULL && !data.done) - g_main_context_iteration (data.context, TRUE); + if (out_www_authenticate) + *out_www_authenticate = g_steal_pointer (&data.hdr_www_authenticate); - if (data.error) - { - g_propagate_error (error, data.error); - return NULL; - } + if (out_status) + *out_status = data.status; - bytes = g_string_free_to_bytes (g_steal_pointer (&content)); - g_debug ("Received %" G_GUINT64_FORMAT " bytes", data.downloaded_bytes); + return g_string_free_to_bytes (g_steal_pointer (&data.content)); + } - return bytes; + g_assert (local_error != NULL); + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; } + GBytes * -flatpak_load_uri (SoupSession *soup_session, +flatpak_load_uri (FlatpakHttpSession *http_session, const char *uri, FlatpakHTTPFlags flags, const char *token, @@ -670,165 +1006,251 @@ flatpak_load_uri (SoupSession *soup_session, GCancellable *cancellable, GError **error) { + return flatpak_load_uri_full (http_session, uri, flags, NULL, token, + progress, user_data, NULL, out_content_type, NULL, + cancellable, error); +} + +gboolean +flatpak_download_http_uri (FlatpakHttpSession *http_session, + const char *uri, + FlatpakHTTPFlags flags, + GOutputStream *out, + const char *token, + FlatpakLoadUriProgress progress, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + g_auto(LoadUriData) data = { NULL }; g_autoptr(GError) local_error = NULL; guint n_retries_remaining = DEFAULT_N_NETWORK_RETRIES; g_autoptr(GMainContextPopDefault) main_context = NULL; + gboolean success = FALSE; main_context = flatpak_main_context_new_default (); - /* Ensure we handle file: uris always */ - if (g_ascii_strncasecmp (uri, "file:", 5) == 0) - { - g_autoptr(GFile) file = g_file_new_for_uri (uri); - gchar *contents; - gsize len; - - if (!g_file_load_contents (file, cancellable, &contents, &len, NULL, error)) - return NULL; + data.context = main_context; + data.progress = progress; + data.user_data = user_data; + data.last_progress_time = g_get_monotonic_time (); + data.cancellable = cancellable; + data.flags = flags; + data.token = token; - return g_bytes_new_take (g_steal_pointer (&contents), len); - } + data.out = out; do { - g_autoptr(GBytes) bytes = NULL; - if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) { g_clear_error (&local_error); - - if (progress) - progress (0, user_data); /* Reset the progress */ + reset_load_uri_data (&data); } - bytes = flatpak_load_http_uri_once (soup_session, uri, flags, - token, progress, user_data, out_content_type, - cancellable, &local_error); + success = flatpak_download_http_uri_once (http_session, &data, uri, &local_error); + + if (success) + break; + + g_assert (local_error != NULL); - if (local_error == NULL) - return g_steal_pointer (&bytes); + /* If the output stream has already been written to we can't retry. + * TODO: use a range request to resume the download */ + if (data.downloaded_bytes > 0) + break; } while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); + if (success) + return TRUE; + g_assert (local_error != NULL); g_propagate_error (error, g_steal_pointer (&local_error)); - return NULL; + return FALSE; } -static gboolean -flatpak_download_http_uri_once (SoupSession *soup_session, - const char *uri, - FlatpakHTTPFlags flags, - GOutputStream *out, - const char *token, - FlatpakLoadUriProgress progress, - gpointer user_data, - guint64 *out_bytes_written, - GCancellable *cancellable, - GError **error) -{ - g_autoptr(SoupRequestHTTP) request = NULL; - g_autoptr(GMainContext) context = NULL; - LoadUriData data = { NULL }; - SoupMessage *m; +/************************************************************************ + * Cached http support * + ***********************************************************************/ - g_debug ("Loading %s using libsoup", uri); +#define CACHE_HTTP_XATTR "user.flatpak.http" +#define CACHE_HTTP_SUFFIX ".flatpak.http" +#define CACHE_HTTP_TYPE "(sstt)" - context = g_main_context_ref_thread_default (); +static void +clear_cache_http_data (CacheHttpData *data, + gboolean clear_uri) +{ + if (clear_uri) + g_clear_pointer (&data->uri, g_free); + g_clear_pointer (&data->etag, g_free); + data->last_modified = 0; + data->expires = 0; +} - data.context = context; - data.out = out; - data.progress = progress; - data.cancellable = cancellable; - data.user_data = user_data; - data.last_progress_time = g_get_monotonic_time (); +static void +free_cache_http_data (CacheHttpData *data) +{ + clear_cache_http_data (data, TRUE); + g_free (data); +} - request = soup_session_request_http (soup_session, "GET", - uri, error); - if (request == NULL) - return FALSE; +G_DEFINE_AUTOPTR_CLEANUP_FUNC (CacheHttpData, free_cache_http_data) - m = soup_request_http_get_message (request); - if (flags & FLATPAK_HTTP_FLAGS_ACCEPT_OCI) - soup_message_headers_replace (m->request_headers, "Accept", - FLATPAK_OCI_MEDIA_TYPE_IMAGE_MANIFEST ", " FLATPAK_DOCKER_MEDIA_TYPE_IMAGE_MANIFEST2); +static GBytes * +serialize_cache_http_data (CacheHttpData * data) +{ + g_autoptr(GVariant) cache_variant = NULL; - if (token) + cache_variant = g_variant_ref_sink (g_variant_new (CACHE_HTTP_TYPE, + data->uri, + data->etag ? data->etag : "", + data->last_modified, + data->expires)); + if (G_BYTE_ORDER != G_BIG_ENDIAN) { - g_autofree char *bearer_token = g_strdup_printf ("Bearer %s", token); - soup_message_headers_replace (m->request_headers, "Authorization", bearer_token); + g_autoptr(GVariant) tmp_variant = cache_variant; + cache_variant = g_variant_byteswap (tmp_variant); } - soup_request_send_async (SOUP_REQUEST (request), - cancellable, - load_uri_callback, &data); - - while (data.error == NULL && !data.done) - g_main_context_iteration (data.context, TRUE); + return g_variant_get_data_as_bytes (cache_variant); +} - if (out_bytes_written) - *out_bytes_written = data.downloaded_bytes; +static void +deserialize_cache_http_data (CacheHttpData *data, + GBytes *bytes) +{ + g_autoptr(GVariant) cache_variant = NULL; - if (data.error) + cache_variant = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE (CACHE_HTTP_TYPE), + bytes, + FALSE)); + if (G_BYTE_ORDER != G_BIG_ENDIAN) { - g_propagate_error (error, data.error); - return FALSE; + g_autoptr(GVariant) tmp_variant = cache_variant; + cache_variant = g_variant_byteswap (tmp_variant); } - g_debug ("Received %" G_GUINT64_FORMAT " bytes", data.downloaded_bytes); - - return TRUE; + g_variant_get (cache_variant, + CACHE_HTTP_TYPE, + &data->uri, + &data->etag, + &data->last_modified, + &data->expires); } -gboolean -flatpak_download_http_uri (SoupSession *soup_session, - const char *uri, - FlatpakHTTPFlags flags, - GOutputStream *out, - const char *token, - FlatpakLoadUriProgress progress, - gpointer user_data, - GCancellable *cancellable, - GError **error) +static CacheHttpData * +load_cache_http_data (int dfd, + char *name, + gboolean *no_xattr, + GCancellable *cancellable, + GError **error) { - g_autoptr(GError) local_error = NULL; - guint n_retries_remaining = DEFAULT_N_NETWORK_RETRIES; - g_autoptr(GMainContextPopDefault) main_context = NULL; + g_autoptr(CacheHttpData) data = NULL; + g_autoptr(GBytes) cache_bytes = glnx_lgetxattrat (dfd, name, + CACHE_HTTP_XATTR, + error); + if (cache_bytes == NULL) + { + if (errno == ENOTSUP) + { + g_autofree char *cache_file = NULL; + glnx_autofd int fd = -1; - main_context = flatpak_main_context_new_default (); + g_clear_error (error); + *no_xattr = TRUE; - do - { - guint64 bytes_written = 0; + cache_file = g_strconcat (name, CACHE_HTTP_SUFFIX, NULL); - if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) - { - g_clear_error (&local_error); + if (!glnx_openat_rdonly (dfd, cache_file, FALSE, + &fd, error)) + return FALSE; - if (progress) - progress (0, user_data); /* Reset the progress */ + cache_bytes = glnx_fd_readall_bytes (fd, cancellable, error); + if (!cache_bytes) + return NULL; } - - if (flatpak_download_http_uri_once (soup_session, uri, flags, - out, token, - progress, user_data, - &bytes_written, - cancellable, &local_error)) + else if (errno == ENOENT || errno == ENODATA) { - g_assert (local_error == NULL); - return TRUE; + g_clear_error (error); + return g_new0 (CacheHttpData, 1); } + else + { + return NULL; + } + } - /* If the output stream has already been written to we can't retry. - * TODO: use a range request to resume the download */ - if (bytes_written > 0) - break; + + data = g_new0 (CacheHttpData, 1); + deserialize_cache_http_data (data, cache_bytes); + return g_steal_pointer (&data); +} + +static gboolean +save_cache_http_data_xattr (int fd, + GBytes *bytes, + GError **error) +{ + if (TEMP_FAILURE_RETRY (fsetxattr (fd, (char *) CACHE_HTTP_XATTR, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes), + 0)) < 0) + return glnx_throw_errno_prefix (error, "fsetxattr"); + + return TRUE; +} + +static gboolean +save_cache_http_data_fallback (int fd, + GBytes *bytes, + GError **error) +{ + if (glnx_loop_write (fd, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes)) < 0) + return glnx_throw_errno_prefix (error, "write"); + + return TRUE; +} + +static gboolean +save_cache_http_data_to_file (int dfd, + char *name, + GBytes *bytes, + gboolean no_xattr, + GCancellable *cancellable, + GError **error) +{ + glnx_autofd int fd = -1; + g_autofree char *fallback_name = NULL; + + if (!no_xattr) + { + if (!glnx_openat_rdonly (dfd, name, FALSE, + &fd, error)) + return FALSE; + + if (save_cache_http_data_xattr (fd, bytes, error)) + return TRUE; + + if (errno == ENOTSUP) + g_clear_error (error); + else + return FALSE; } - while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); - g_assert (local_error != NULL); - g_propagate_error (error, g_steal_pointer (&local_error)); - return FALSE; + fallback_name = g_strconcat (name, CACHE_HTTP_SUFFIX, NULL); + if (!glnx_file_replace_contents_at (dfd, fallback_name, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes), + 0, + cancellable, + error)) + return FALSE; + + return TRUE; } static gboolean @@ -863,34 +1285,119 @@ sync_and_rename_tmpfile (GLnxTmpfile *tmpfile, return TRUE; } -static gboolean -flatpak_cache_http_uri_once (SoupSession *soup_session, - const char *uri, - FlatpakHTTPFlags flags, - int dest_dfd, - const char *dest_subpath, - FlatpakLoadUriProgress progress, - gpointer user_data, - GCancellable *cancellable, - GError **error) +static void +set_cache_http_data_from_headers (CacheHttpData *cache_data, + LoadUriData *data) { - g_autoptr(SoupRequestHTTP) request = NULL; - g_autoptr(GMainContext) context = NULL; + const char *etag = data->hdr_etag; + const char *last_modified = data->hdr_last_modified; + const char *cache_control = data->hdr_cache_control; + const char *expires = data->hdr_expires; + gboolean expires_computed = FALSE; + + /* The original HTTP 1/1 specification only required sending the ETag header in a 304 + * response, and implied that a cache might need to save the old Cache-Control + * values. The updated RFC 7232 from 2014 requires sending Cache-Control, ETags, and + * Expire if they would have been sent in the original 200 response, and recommends + * sending Last-Modified for requests without an etag. Since sending these headers was + * apparently normal previously, for simplicity we assume the RFC 7232 behavior and start + * from scratch for a 304 response. + */ + clear_cache_http_data (cache_data, FALSE); + + if (etag && *etag) + { + cache_data->etag = g_strdup (etag); + } + else if (last_modified && *last_modified) + { + g_autoptr(GDateTime) date = flatpak_parse_http_time (last_modified); + if (date) + cache_data->last_modified = g_date_time_to_unix (date); + } + + if (cache_control && *cache_control) + { + g_autoptr(GHashTable) params = flatpak_parse_http_header_param_list (cache_control); + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, params); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if (g_strcmp0 (key, "max-age") == 0) + { + char *end; + + char *max_age = value; + int max_age_sec = g_ascii_strtoll (max_age, &end, 10); + if (*max_age != '\0' && *end == '\0') + { + GTimeVal now; + g_get_current_time (&now); + cache_data->expires = now.tv_sec + max_age_sec; + expires_computed = TRUE; + } + } + else if (g_strcmp0 (key, "no-cache") == 0) + { + cache_data->expires = 0; + expires_computed = TRUE; + } + } + } + + if (!expires_computed && expires && *expires) + { + g_autoptr(GDateTime) date = flatpak_parse_http_time (expires); + if (date) + { + cache_data->expires = g_date_time_to_unix (date); + expires_computed = TRUE; + } + } + + if (!expires_computed) + { + /* If nothing implies an expires time, use 30 minutes. Browsers use + * 0.1 * (Date - Last-Modified), but it's clearly appropriate here, and + * better if server's send a value. + */ + GTimeVal now; + g_get_current_time (&now); + cache_data->expires = now.tv_sec + 1800; + } +} + +gboolean +flatpak_cache_http_uri (FlatpakHttpSession *http_session, + const char *uri, + FlatpakHTTPFlags flags, + int dest_dfd, + const char *dest_subpath, + FlatpakLoadUriProgress progress, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + g_auto(LoadUriData) data = { NULL }; + g_autoptr(GError) local_error = NULL; + guint n_retries_remaining = DEFAULT_N_NETWORK_RETRIES; g_autoptr(CacheHttpData) cache_data = NULL; + g_autoptr(GMainContextPopDefault) main_context = NULL; g_autofree char *parent_path = g_path_get_dirname (dest_subpath); g_autofree char *name = g_path_get_basename (dest_subpath); - glnx_autofd int dfd = -1; - gboolean no_xattr = FALSE; - LoadUriData data = { NULL }; g_auto(GLnxTmpfile) out_tmpfile = { 0 }; g_auto(GLnxTmpfile) cache_tmpfile = { 0 }; g_autoptr(GBytes) cache_bytes = NULL; - SoupMessage *m; + gboolean no_xattr = FALSE; + glnx_autofd int cache_dfd = -1; + gboolean success; - if (!glnx_opendirat (dest_dfd, parent_path, TRUE, &dfd, error)) + if (!glnx_opendirat (dest_dfd, parent_path, TRUE, &cache_dfd, error)) return FALSE; - cache_data = load_cache_http_data (dfd, name, &no_xattr, + cache_data = load_cache_http_data (cache_dfd, name, &no_xattr, cancellable, error); if (!cache_data) return FALSE; @@ -905,10 +1412,9 @@ flatpak_cache_http_uri_once (SoupSession *soup_session, g_get_current_time (&now); if (cache_data->expires > now.tv_sec) { - if (error) - *error = g_error_new (FLATPAK_HTTP_ERROR, - FLATPAK_HTTP_ERROR_NOT_CHANGED, - "Reusing cached value"); + g_set_error (error, FLATPAK_HTTP_ERROR, + FLATPAK_HTTP_ERROR_NOT_CHANGED, + "Reusing cached value"); return FALSE; } } @@ -916,83 +1422,66 @@ flatpak_cache_http_uri_once (SoupSession *soup_session, if (cache_data->uri == NULL) cache_data->uri = g_strdup (uri); - /* Must revalidate */ - - g_debug ("Loading %s using libsoup", uri); + /* Missing from cache, or expired so must revalidate via etag/last-modified headers */ - context = g_main_context_ref_thread_default (); + main_context = flatpak_main_context_new_default (); - data.context = context; - data.cache_data = cache_data; - data.out_tmpfile = &out_tmpfile; - data.out_tmpfile_parent_dfd = dfd; + data.context = main_context; data.progress = progress; - data.cancellable = cancellable; data.user_data = user_data; data.last_progress_time = g_get_monotonic_time (); + data.cancellable = cancellable; + data.flags = flags; - request = soup_session_request_http (soup_session, "GET", - uri, error); - if (request == NULL) - return FALSE; + data.cache_data = cache_data; - m = soup_request_http_get_message (request); + data.out_tmpfile = &out_tmpfile; + data.out_tmpfile_parent_dfd = cache_dfd; - if (cache_data->etag && cache_data->etag[0]) - soup_message_headers_replace (m->request_headers, "If-None-Match", cache_data->etag); - else if (cache_data->last_modified != 0) + do { - SoupDate *date = soup_date_new_from_time_t (cache_data->last_modified); - g_autofree char *date_str = soup_date_to_string (date, SOUP_DATE_HTTP); - soup_message_headers_replace (m->request_headers, "If-Modified-Since", date_str); - soup_date_free (date); - } + if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) + { + g_clear_error (&local_error); + reset_load_uri_data (&data); + } - if (flags & FLATPAK_HTTP_FLAGS_ACCEPT_OCI) - soup_message_headers_replace (m->request_headers, "Accept", - FLATPAK_OCI_MEDIA_TYPE_IMAGE_MANIFEST ", " FLATPAK_DOCKER_MEDIA_TYPE_IMAGE_MANIFEST2); + success = flatpak_download_http_uri_once (http_session, &data, uri, &local_error); - if (flags & FLATPAK_HTTP_FLAGS_STORE_COMPRESSED) - { - soup_session_remove_feature_by_type (soup_session, SOUP_TYPE_CONTENT_DECODER); - soup_message_headers_replace (m->request_headers, "Accept-Encoding", - "gzip"); - data.store_compressed = TRUE; - } - else if (!soup_session_has_feature (soup_session, SOUP_TYPE_CONTENT_DECODER)) - soup_session_add_feature_by_type (soup_session, SOUP_TYPE_CONTENT_DECODER); + if (success) + break; - soup_request_send_async (SOUP_REQUEST (request), - cancellable, - load_uri_callback, &data); + g_assert (local_error != NULL); + } + while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); - while (data.error == NULL && !data.done) - g_main_context_iteration (data.context, TRUE); + /* Update the cache data on success or cache-valid */ + if (success || g_error_matches (local_error, FLATPAK_HTTP_ERROR, FLATPAK_HTTP_ERROR_NOT_CHANGED)) + { + set_cache_http_data_from_headers (cache_data, &data); + cache_bytes = serialize_cache_http_data (cache_data); + } - if (data.error) + if (local_error) { - if (data.error->domain == FLATPAK_HTTP_ERROR && - data.error->code == FLATPAK_HTTP_ERROR_NOT_CHANGED) + if (cache_bytes) { GError *tmp_error = NULL; - cache_bytes = serialize_cache_http_data (cache_data); - - if (!save_cache_http_data_to_file (dfd, name, cache_bytes, no_xattr, + if (!save_cache_http_data_to_file (cache_dfd, name, cache_bytes, no_xattr, cancellable, &tmp_error)) { - g_clear_error (&data.error); + g_clear_error (&local_error); g_propagate_error (error, tmp_error); return FALSE; } } - g_propagate_error (error, data.error); + g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } - cache_bytes = serialize_cache_http_data (cache_data); if (!no_xattr) { if (!save_cache_http_data_xattr (out_tmpfile.fd, cache_bytes, error)) @@ -1007,7 +1496,7 @@ flatpak_cache_http_uri_once (SoupSession *soup_session, if (no_xattr) { - if (!glnx_open_tmpfile_linkable_at (dfd, ".", O_WRONLY, &cache_tmpfile, error)) + if (!glnx_open_tmpfile_linkable_at (cache_dfd, ".", O_WRONLY, &cache_tmpfile, error)) return FALSE; if (!save_cache_http_data_fallback (cache_tmpfile.fd, cache_bytes, error)) @@ -1025,50 +1514,5 @@ flatpak_cache_http_uri_once (SoupSession *soup_session, return FALSE; } - g_debug ("Received %" G_GUINT64_FORMAT " bytes", data.downloaded_bytes); - return TRUE; } - -gboolean -flatpak_cache_http_uri (SoupSession *soup_session, - const char *uri, - FlatpakHTTPFlags flags, - int dest_dfd, - const char *dest_subpath, - FlatpakLoadUriProgress progress, - gpointer user_data, - GCancellable *cancellable, - GError **error) -{ - g_autoptr(GError) local_error = NULL; - guint n_retries_remaining = DEFAULT_N_NETWORK_RETRIES; - g_autoptr(GMainContextPopDefault) main_context = NULL; - - main_context = flatpak_main_context_new_default (); - - do - { - if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) - { - g_clear_error (&local_error); - - if (progress) - progress (0, user_data); /* Reset the progress */ - } - - if (flatpak_cache_http_uri_once (soup_session, uri, flags, - dest_dfd, dest_subpath, - progress, user_data, - cancellable, &local_error)) - { - g_assert (local_error == NULL); - return TRUE; - } - } - while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); - - g_assert (local_error != NULL); - g_propagate_error (error, g_steal_pointer (&local_error)); - return FALSE; -} |