/* 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 * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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: * Alexander Larsson */ #include "config.h" #include #include #include "flatpak-utils-http-private.h" #include "flatpak-uri-private.h" #include "flatpak-oci-registry-private.h" #include #include "libglnx.h" #include #include #if defined(HAVE_CURL) #include /* 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 #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; char *etag; gint64 last_modified; gint64 expires; } CacheHttpData; typedef struct { GMainContext *context; gboolean done; GError *error; /* 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; /* Used during operation */ char buffer[16 * 1024]; guint64 last_progress_time; gboolean store_compressed; } LoadUriData; 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 reset_load_uri_data (LoadUriData *data) { 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 clear_load_uri_data (LoadUriData *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_AUTO_CLEANUP_CLEAR_FUNC(LoadUriData, clear_load_uri_data) static gboolean check_http_status (guint status_code, GError **error) { GQuark domain; int code; if (status_code >= 200 && status_code < 300) return TRUE; switch (status_code) { 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; } 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 check_header(char **value_out, const char *header, char *buffer, size_t realsize) { size_t hlen = strlen (header); 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)) { buffer++; realsize--; } 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 size_t _header_cb (char *buffer, size_t size, size_t nitems, void *userdata) { size_t realsize = size * nitems; LoadUriData *data = (LoadUriData *)userdata; check_header(&data->hdr_content_type, "content-type", buffer, realsize); check_header(&data->hdr_www_authenticate, "WWW-Authenticate", buffer, realsize); 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); return realsize; } 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 */ } out = g_unix_output_stream_new (data->out_tmpfile->fd, FALSE); if (data->store_compressed && g_strcmp0 (data->hdr_content_encoding, "gzip") != 0) { 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 { 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->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; } FlatpakHttpSession * flatpak_create_http_session (const char *user_agent) { FlatpakHttpSession *session = g_new0 (FlatpakHttpSession, 1); CURLcode rc; CURL *curl; session->curl = curl = curl_easy_init(); g_assert (session->curl != NULL); g_mutex_init (&session->lock); curl_easy_setopt (curl, CURLOPT_USERAGENT, user_agent); #if CURL_AT_LEAST_VERSION(7, 85, 0) rc = curl_easy_setopt (curl, CURLOPT_PROTOCOLS_STR, "http,https"); #else rc = curl_easy_setopt (curl, CURLOPT_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS)); #endif 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. */ #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 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _write_cb); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, _header_cb); /* Abort the connection if connecting to the server takes too long. This * timeout has no effect after a connection is established. */ curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, (long)FLATPAK_HTTP_TIMEOUT_SECS); /* Abort the download if it’s slower than 10KB/sec for 60 seconds. An example * compressed summary file is 1.5MB in size, so anything slower than this rate * will mean it takes over 2.5 minutes to download just the summary file. */ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, (long)FLATPAK_HTTP_TIMEOUT_SECS); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 10000L); 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) { 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; } 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) { 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) { 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); } curl_easy_setopt (curl, CURLOPT_HTTPHEADER, header_list); if (data->flags & FLATPAK_HTTP_FLAGS_STORE_COMPRESSED) { 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; } res = curl_easy_perform (session->curl); curl_easy_setopt (session->curl, CURLOPT_HTTPHEADER, NULL); /* Don't point to freed list */ if (res != CURLE_OK) { 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 (data->out_tmpfile && data->out) { /* Flush the writes */ if (!g_output_stream_close (data->out, data->cancellable, error)) return FALSE; g_clear_pointer (&data->out, g_object_unref); } if (data->progress) data->progress (data->downloaded_bytes, data->user_data); curl_easy_getinfo (session->curl, CURLINFO_RESPONSE_CODE, &response); 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 check_soup_transfer_error (SoupMessage *msg, GError **error) { GQuark domain = G_IO_ERROR; int code; if (!SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code)) return TRUE; switch (msg->status_code) { 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_IO_ERROR: code = G_IO_ERROR_CONNECTION_CLOSED; break; default: code = G_IO_ERROR_FAILED; } 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) { LoadUriData *data = user_data; GInputStream *stream = G_INPUT_STREAM (source); g_autoptr(GError) error = NULL; if (!g_input_stream_close_finish (stream, res, &error)) g_warning ("Error closing http stream: %s", error->message); if (data->out_tmpfile) { if (!g_output_stream_close (data->out, data->cancellable, &error)) { if (data->error == NULL) g_propagate_error (&data->error, g_steal_pointer (&error)); } g_clear_pointer (&data->out, g_object_unref); } data->done = TRUE; 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) { LoadUriData *data = user_data; GInputStream *stream = G_INPUT_STREAM (source); gssize nread; nread = g_input_stream_read_finish (stream, res, &data->error); if (nread == -1 || nread == 0) { if (data->progress) data->progress (data->downloaded_bytes, data->user_data); g_input_stream_close_async (stream, G_PRIORITY_DEFAULT, NULL, stream_closed, data); return; } if (data->out != NULL) { gsize n_written; if (!g_output_stream_write_all (data->out, data->buffer, nread, &n_written, NULL, &data->error)) { data->downloaded_bytes += n_written; g_input_stream_close_async (stream, G_PRIORITY_DEFAULT, NULL, stream_closed, data); return; } data->downloaded_bytes += n_written; } else { g_assert (data->content != NULL); data->downloaded_bytes += nread; g_string_append_len (data->content, data->buffer, nread); } 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 (); } g_input_stream_read_async (stream, data->buffer, sizeof (data->buffer), G_PRIORITY_DEFAULT, data->cancellable, load_uri_read_cb, data); } /* The http header part of the request is ready */ static void load_uri_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { SoupRequestHTTP *request = SOUP_REQUEST_HTTP (source_object); g_autoptr(GInputStream) in = NULL; LoadUriData *data = user_data; in = soup_request_send_finish (SOUP_REQUEST (request), res, &data->error); if (in == NULL) { g_main_context_wakeup (data->context); return; } g_autoptr(SoupMessage) msg = soup_request_http_get_message ((SoupRequestHTTP *) request); if (!check_soup_transfer_error (msg, &data->error)) { g_main_context_wakeup (data->context); return; } /* 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. */ 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")); if ((data->flags & FLATPAK_HTTP_FLAGS_NOCHECK_STATUS) == 0 && !check_http_status (data->status, &data->error)) { g_main_context_wakeup (data->context); return; } /* All is good, write the body to the destination */ if (data->out_tmpfile) { g_autoptr(GOutputStream) out = NULL; if (!glnx_open_tmpfile_linkable_at (data->out_tmpfile_parent_dfd, ".", O_WRONLY, data->out_tmpfile, &data->error)) { g_main_context_wakeup (data->context); return; } g_assert (data->out == NULL); out = g_unix_output_stream_new (data->out_tmpfile->fd, FALSE); if (data->store_compressed && g_strcmp0 (soup_message_headers_get_one (msg->response_headers, "Content-Encoding"), "gzip") != 0) { 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 { data->out = g_steal_pointer (&out); } } g_input_stream_read_async (in, data->buffer, sizeof (data->buffer), G_PRIORITY_DEFAULT, data->cancellable, load_uri_read_cb, data); } static SoupSession * flatpak_create_soup_session (const char *user_agent) { SoupSession *soup_session; const char *http_proxy; 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, FLATPAK_HTTP_TIMEOUT_SECS, SOUP_SESSION_IDLE_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS, NULL); http_proxy = g_getenv ("http_proxy"); if (http_proxy) { g_autoptr(SoupURI) proxy_uri = soup_uri_new (http_proxy); if (!proxy_uri) g_warning ("Invalid proxy URI '%s'", http_proxy); else g_object_set (soup_session, SOUP_SESSION_PROXY_URI, proxy_uri, NULL); } if (g_getenv ("OSTREE_DEBUG_HTTP")) soup_session_add_feature (soup_session, (SoupSessionFeature *) soup_logger_new (SOUP_LOGGER_LOG_BODY, 500)); 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 * created, and must be decremented by the caller. (@n_retries_remaining == 0) * will always return %FALSE from this function. * * This code is copied from libostree's _ostree_fetcher_should_retry_request() */ static gboolean flatpak_http_should_retry_request (const GError *error, guint n_retries_remaining) { if (error == NULL || n_retries_remaining == 0) return FALSE; /* Return TRUE for transient errors. */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) || 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) || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED) || 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_info ("Should retry request (remaining: %u retries), due to transient error: %s", n_retries_remaining, error->message); return TRUE; } return FALSE; } 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) { 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; /* 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; 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 = 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.auth = auth; data.token = token; data.content = g_string_new (""); do { if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) { g_clear_error (&local_error); reset_load_uri_data (&data); } success = flatpak_download_http_uri_once (http_session, &data, uri, &local_error); if (success) break; g_assert (local_error != NULL); } while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); if (success) { if (out_content_type) *out_content_type = g_steal_pointer (&data.hdr_content_type); if (out_www_authenticate) *out_www_authenticate = g_steal_pointer (&data.hdr_www_authenticate); if (out_status) *out_status = data.status; return g_string_free_to_bytes (g_steal_pointer (&data.content)); } g_assert (local_error != NULL); g_propagate_error (error, g_steal_pointer (&local_error)); return NULL; } GBytes * flatpak_load_uri (FlatpakHttpSession *http_session, const char *uri, FlatpakHTTPFlags flags, const char *token, FlatpakLoadUriProgress progress, gpointer user_data, char **out_content_type, 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 (); 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; data.out = out; do { if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) { g_clear_error (&local_error); reset_load_uri_data (&data); } success = flatpak_download_http_uri_once (http_session, &data, uri, &local_error); if (success) break; g_assert (local_error != NULL); /* 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 FALSE; } /************************************************************************ * Cached http support * ***********************************************************************/ #define CACHE_HTTP_XATTR "user.flatpak.http" #define CACHE_HTTP_SUFFIX ".flatpak.http" #define CACHE_HTTP_TYPE "(sstt)" 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; } static void free_cache_http_data (CacheHttpData *data) { clear_cache_http_data (data, TRUE); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (CacheHttpData, free_cache_http_data) static GBytes * serialize_cache_http_data (CacheHttpData * data) { g_autoptr(GVariant) cache_variant = NULL; 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_autoptr(GVariant) tmp_variant = cache_variant; cache_variant = g_variant_byteswap (tmp_variant); } return g_variant_get_data_as_bytes (cache_variant); } static void deserialize_cache_http_data (CacheHttpData *data, GBytes *bytes) { g_autoptr(GVariant) cache_variant = NULL; 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_autoptr(GVariant) tmp_variant = cache_variant; cache_variant = g_variant_byteswap (tmp_variant); } g_variant_get (cache_variant, CACHE_HTTP_TYPE, &data->uri, &data->etag, &data->last_modified, &data->expires); } static CacheHttpData * load_cache_http_data (int dfd, char *name, gboolean *no_xattr, GCancellable *cancellable, GError **error) { 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; g_clear_error (error); *no_xattr = TRUE; cache_file = g_strconcat (name, CACHE_HTTP_SUFFIX, NULL); if (!glnx_openat_rdonly (dfd, cache_file, FALSE, &fd, error)) return FALSE; cache_bytes = glnx_fd_readall_bytes (fd, cancellable, error); if (!cache_bytes) return NULL; } else if (errno == ENOENT || errno == ENODATA) { g_clear_error (error); return g_new0 (CacheHttpData, 1); } else { return NULL; } } 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; } 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 sync_and_rename_tmpfile (GLnxTmpfile *tmpfile, const char *dest_name, GError **error) { /* Filesystem paranoia: If we end up with the new metadata but not * the new data, then because the cache headers are in the metadata, * we'll never re-download. (If we just want to avoid losing both * the old and new data, skipping fdatasync when the destination is * missing works, but it won't here.) * * This will cause a bunch of fdatasyncs when downloading the icons for * a large appstream the first time, would mostly be a problem with a * very fast internet connection and a slow spinning drive. * Possible solution: update in new directory without fdatasync * (copying in any existing cached icons to revalidate), syncfs(), then * atomic symlink. */ if (fdatasync (tmpfile->fd) != 0) return glnx_throw_errno_prefix (error, "fdatasync"); if (fchmod (tmpfile->fd, 0644) != 0) return glnx_throw_errno_prefix (error, "fchmod"); if (!glnx_link_tmpfile_at (tmpfile, GLNX_LINK_TMPFILE_REPLACE, tmpfile->src_dfd, dest_name, error)) return FALSE; return TRUE; } static void set_cache_http_data_from_headers (CacheHttpData *cache_data, LoadUriData *data) { 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); g_auto(GLnxTmpfile) out_tmpfile = { 0 }; g_auto(GLnxTmpfile) cache_tmpfile = { 0 }; g_autoptr(GBytes) cache_bytes = NULL; gboolean no_xattr = FALSE; glnx_autofd int cache_dfd = -1; gboolean success; if (!glnx_opendirat (dest_dfd, parent_path, TRUE, &cache_dfd, error)) return FALSE; cache_data = load_cache_http_data (cache_dfd, name, &no_xattr, cancellable, error); if (!cache_data) return FALSE; if (g_strcmp0 (cache_data->uri, uri) != 0) clear_cache_http_data (cache_data, TRUE); if (cache_data->uri) { GTimeVal now; g_get_current_time (&now); if (cache_data->expires > now.tv_sec) { g_set_error (error, FLATPAK_HTTP_ERROR, FLATPAK_HTTP_ERROR_NOT_CHANGED, "Reusing cached value"); return FALSE; } } if (cache_data->uri == NULL) cache_data->uri = g_strdup (uri); /* Missing from cache, or expired so must revalidate via etag/last-modified headers */ main_context = flatpak_main_context_new_default (); 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.cache_data = cache_data; data.out_tmpfile = &out_tmpfile; data.out_tmpfile_parent_dfd = cache_dfd; do { if (n_retries_remaining < DEFAULT_N_NETWORK_RETRIES) { g_clear_error (&local_error); reset_load_uri_data (&data); } success = flatpak_download_http_uri_once (http_session, &data, uri, &local_error); if (success) break; g_assert (local_error != NULL); } while (flatpak_http_should_retry_request (local_error, n_retries_remaining--)); /* 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 (local_error) { if (cache_bytes) { GError *tmp_error = NULL; if (!save_cache_http_data_to_file (cache_dfd, name, cache_bytes, no_xattr, cancellable, &tmp_error)) { g_clear_error (&local_error); g_propagate_error (error, tmp_error); return FALSE; } } g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } if (!no_xattr) { if (!save_cache_http_data_xattr (out_tmpfile.fd, cache_bytes, error)) { if (errno != ENOTSUP) return FALSE; g_clear_error (error); no_xattr = TRUE; } } if (no_xattr) { 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)) return FALSE; } if (!sync_and_rename_tmpfile (&out_tmpfile, name, error)) return FALSE; if (no_xattr) { g_autofree char *fallback_name = g_strconcat (name, CACHE_HTTP_SUFFIX, NULL); if (!sync_and_rename_tmpfile (&cache_tmpfile, fallback_name, error)) return FALSE; } return TRUE; }