/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* 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 .
*/
/**
* SECTION: e-soup-session
* @include: libedataserver/libedataserver.h
* @short_description: A SoupSession descendant
*
* The #ESoupSession is a #SoupSession descendant, which hides common
* tasks related to the way evolution-data-server works.
**/
#include "evolution-data-server-config.h"
#include
#include
#include "e-data-server-util.h"
#include "e-oauth2-services.h"
#include "e-soup-auth-bearer.h"
#include "e-soup-ssl-trust.h"
#include "e-source-authentication.h"
#include "e-source-webdav.h"
#include "e-soup-session.h"
G_DEFINE_QUARK (e-soup-session-error-quark, e_soup_session_error)
#define E_SOUP_SESSION_MESSAGE_BYTES_KEY "e-soup-session-message-bytes"
#define BUFFER_SIZE 16384
struct _ESoupSessionPrivate {
GMutex property_lock;
GRecMutex session_lock; /* libsoup3 has no thread safety */
GHashTable *using_auths; /* guarded by the session_lock, gchar *uri ~> gchar *authtype, as set in the SoupAuthManager */
ESource *source;
ENamedParameters *credentials;
gboolean ssl_info_set;
gchar *ssl_certificate_pem;
GTlsCertificateFlags ssl_certificate_errors;
SoupLoggerLogLevel log_level;
GError *bearer_auth_error;
ESoupAuthBearer *using_bearer_auth;
gboolean auth_prefilled; /* When TRUE, the first 'retrying' is ignored in the "authenticate" handler */
gboolean force_http1;
};
enum {
PROP_0,
PROP_SOURCE,
PROP_CREDENTIALS,
PROP_FORCE_HTTP1
};
G_DEFINE_TYPE_WITH_PRIVATE (ESoupSession, e_soup_session, SOUP_TYPE_SESSION)
/* Hold the session lock when calling this */
static gboolean
e_soup_session_auth_already_set_locked (ESoupSession *session,
GUri *g_uri,
SoupAuth *soup_auth,
gboolean *out_auth_was_set)
{
gchar *uri_str;
const gchar *auth_type;
const gchar *current_auth_type;
gboolean same_types;
uri_str = g_uri_to_string_partial (g_uri, G_URI_HIDE_PASSWORD);
auth_type = G_OBJECT_TYPE_NAME (soup_auth);
current_auth_type = g_hash_table_lookup (session->priv->using_auths, uri_str);
*out_auth_was_set = current_auth_type != NULL;
same_types = g_strcmp0 (auth_type, current_auth_type) == 0;
if (!same_types) {
/* Because the caller calls soup_auth_manager_clear_cached_credentials() in this case */
if (*out_auth_was_set)
g_hash_table_remove_all (session->priv->using_auths);
g_hash_table_insert (session->priv->using_auths, uri_str, g_strdup (auth_type));
uri_str = NULL;
}
g_free (uri_str);
return same_types;
}
static void
e_soup_session_ensure_auth_usage (ESoupSession *session,
GUri *in_g_uri,
SoupMessage *message,
SoupAuth *soup_auth)
{
SoupAuthManager *auth_manager;
SoupSessionFeature *feature;
GUri *g_uri;
GType auth_type;
gboolean auth_was_set = FALSE;
g_return_if_fail (E_IS_SOUP_SESSION (session));
g_return_if_fail (SOUP_IS_AUTH (soup_auth));
g_rec_mutex_lock (&session->priv->session_lock);
feature = soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_AUTH_MANAGER);
auth_type = G_OBJECT_TYPE (soup_auth);
if (!soup_session_has_feature (SOUP_SESSION (session), auth_type)) {
/* Add the SoupAuth type to support it. */
soup_session_add_feature_by_type (SOUP_SESSION (session), auth_type);
}
if (in_g_uri) {
g_uri = in_g_uri;
} else {
g_uri = message ? soup_message_get_uri (message) : NULL;
if (g_uri && g_uri_get_host (g_uri) && *g_uri_get_host (g_uri)) {
g_uri = g_uri_build (SOUP_HTTP_URI_FLAGS,
g_uri_get_scheme (g_uri),
NULL,
g_uri_get_host (g_uri),
g_uri_get_port (g_uri),
"",
NULL,
NULL);
} else {
g_uri = NULL;
}
if (!g_uri) {
ESource *source;
source = e_soup_session_get_source (session);
if (source) {
ESourceWebdav *extension;
extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
g_uri = e_source_webdav_dup_uri (extension);
}
}
}
auth_manager = SOUP_AUTH_MANAGER (feature);
/* This will make sure the 'soup_auth' is used regardless of the current 'auth_manager' state,
but do not set the same 'soup_auth' when it's already set (which can happen, when the session
is reused by multiple sources, which connect to the same server, with the same user.
See https://gitlab.gnome.org/GNOME/libsoup/-/issues/196 for more information. */
if (g_uri && !e_soup_session_auth_already_set_locked (session, g_uri, soup_auth, &auth_was_set)) {
if (auth_was_set)
soup_auth_manager_clear_cached_credentials (auth_manager);
soup_auth_manager_use_auth (auth_manager, g_uri, soup_auth);
}
g_rec_mutex_unlock (&session->priv->session_lock);
if (!in_g_uri && g_uri)
g_uri_unref (g_uri);
}
static gboolean
e_soup_session_setup_bearer_auth (ESoupSession *session,
SoupMessage *message,
gboolean is_in_authenticate_handler,
ESoupAuthBearer *bearer,
GCancellable *cancellable,
GError **error)
{
ESource *source;
gchar *access_token = NULL;
gint expires_in_seconds = -1;
gboolean success = FALSE;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
source = e_soup_session_get_source (session);
if (!source) {
/* Do not localize this error message, it should not get into the UI */
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "No ESource specified");
return FALSE;
}
success = e_source_get_oauth2_access_token_sync (source, cancellable,
&access_token, &expires_in_seconds, error);
if (success) {
e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
/* Preload the SoupAuthManager with a valid "Bearer" token
* when using OAuth 2.0. This avoids an extra unauthorized
* HTTP round-trip, which apparently Google doesn't like. */
if (!is_in_authenticate_handler)
e_soup_session_ensure_auth_usage (session, NULL, message, SOUP_AUTH (bearer));
}
g_free (access_token);
return success;
}
static gboolean
e_soup_session_maybe_prepare_bearer_auth (ESoupSession *session,
GUri *g_uri,
SoupMessage *message,
GCancellable *cancellable,
GError **error)
{
gboolean success;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (g_uri != NULL, FALSE);
g_mutex_lock (&session->priv->property_lock);
if (session->priv->using_bearer_auth) {
ESoupAuthBearer *using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
g_mutex_unlock (&session->priv->property_lock);
success = e_soup_session_setup_bearer_auth (session, message, FALSE, using_bearer_auth, cancellable, error);
g_clear_object (&using_bearer_auth);
} else {
ESoupAuthBearer *soup_auth;
g_mutex_unlock (&session->priv->property_lock);
soup_auth = g_object_new (
E_TYPE_SOUP_AUTH_BEARER,
"authority", g_uri_get_host (g_uri), NULL);
success = e_soup_session_setup_bearer_auth (session, message, FALSE, E_SOUP_AUTH_BEARER (soup_auth), cancellable, error);
if (success) {
g_mutex_lock (&session->priv->property_lock);
g_clear_object (&session->priv->using_bearer_auth);
session->priv->using_bearer_auth = g_object_ref (soup_auth);
g_mutex_unlock (&session->priv->property_lock);
}
g_object_unref (soup_auth);
}
return success;
}
static gboolean
e_soup_session_maybe_prepare_basic_auth (ESoupSession *session,
GUri *g_uri,
SoupMessage *message,
const gchar *in_username,
const ENamedParameters *credentials,
GCancellable *cancellable,
GError **error)
{
SoupAuth *soup_auth;
const gchar *username;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (g_uri != NULL, FALSE);
if (!credentials || !e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD)) {
/* This error message won't get into the UI */
g_set_error_literal (error, E_SOUP_SESSION_ERROR, SOUP_STATUS_UNAUTHORIZED, soup_status_get_phrase (SOUP_STATUS_UNAUTHORIZED));
return FALSE;
}
username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
if (!username || !*username)
username = in_username;
soup_auth = soup_auth_new (SOUP_TYPE_AUTH_BASIC, message, "Basic");
soup_auth_authenticate (soup_auth, username, e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD));
g_mutex_lock (&session->priv->property_lock);
session->priv->auth_prefilled = TRUE;
g_mutex_unlock (&session->priv->property_lock);
e_soup_session_ensure_auth_usage (session, g_uri, message, soup_auth);
g_clear_object (&soup_auth);
return TRUE;
}
static gboolean
e_soup_session_maybe_prepare_auth (ESoupSession *session,
SoupMessage *message,
GCancellable *cancellable,
GError **error)
{
ESource *source;
ENamedParameters *credentials;
GUri *g_uri;
gchar *auth_method = NULL, *user = NULL;
gboolean success = TRUE;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
source = e_soup_session_get_source (session);
if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
ESourceAuthentication *extension;
extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
auth_method = e_source_authentication_dup_method (extension);
user = e_source_authentication_dup_user (extension);
} else {
return TRUE;
}
credentials = e_soup_session_dup_credentials (session);
g_uri = message ? soup_message_get_uri (message) : NULL;
if (g_uri && g_uri_get_host (g_uri) && *g_uri_get_host (g_uri)) {
g_uri = g_uri_build (SOUP_HTTP_URI_FLAGS,
g_uri_get_scheme (g_uri),
NULL,
g_uri_get_host (g_uri),
g_uri_get_port (g_uri),
"",
NULL,
NULL);
} else {
g_uri = NULL;
}
if (!g_uri) {
ESourceWebdav *extension;
extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
g_uri = e_source_webdav_dup_uri (extension);
}
g_mutex_lock (&session->priv->property_lock);
session->priv->auth_prefilled = FALSE;
g_mutex_unlock (&session->priv->property_lock);
/* Provide credentials beforehand only on secure connections */
if (g_strcmp0 (g_uri_get_scheme (g_uri), "https") == 0) {
if (g_strcmp0 (auth_method, "OAuth2") == 0 ||
e_oauth2_services_is_oauth2_alias_static (auth_method)) {
success = e_soup_session_maybe_prepare_bearer_auth (session, g_uri, message, cancellable, error);
} else if (g_strcmp0 (auth_method, "GSSAPI") == 0 && soup_auth_negotiate_supported ()) {
SoupSession *soup_session = SOUP_SESSION (session);
g_rec_mutex_lock (&session->priv->session_lock);
if (!soup_session_get_feature (soup_session, SOUP_TYPE_AUTH_NEGOTIATE))
soup_session_add_feature_by_type (soup_session, SOUP_TYPE_AUTH_NEGOTIATE);
if (soup_session_get_feature (soup_session, SOUP_TYPE_AUTH_BASIC))
soup_session_remove_feature_by_type (soup_session, SOUP_TYPE_AUTH_BASIC);
g_rec_mutex_unlock (&session->priv->session_lock);
} else if (g_strcmp0 (auth_method, "NTLM") == 0) {
SoupSession *soup_session = SOUP_SESSION (session);
g_rec_mutex_lock (&session->priv->session_lock);
if (!soup_session_get_feature (soup_session, SOUP_TYPE_AUTH_NTLM))
soup_session_add_feature_by_type (soup_session, SOUP_TYPE_AUTH_NTLM);
/* Keep the basic auth, as a fallback */
g_rec_mutex_unlock (&session->priv->session_lock);
} else if (user && *user) {
/* Default to Basic authentication when user is filled */
success = e_soup_session_maybe_prepare_basic_auth (session, g_uri, message, user, credentials, cancellable, error);
}
}
e_named_parameters_free (credentials);
g_uri_unref (g_uri);
g_free (auth_method);
g_free (user);
return success;
}
static gboolean
e_soup_session_authenticate_cb (SoupMessage *message,
SoupAuth *auth,
gboolean retrying,
gpointer user_data)
{
ESoupSession *session = user_data;
const gchar *username;
ENamedParameters *credentials;
gchar *auth_user = NULL;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_mutex_lock (&session->priv->property_lock);
if (E_IS_SOUP_AUTH_BEARER (auth)) {
g_object_ref (auth);
g_warn_if_fail ((gpointer) session->priv->using_bearer_auth == (gpointer) auth);
g_clear_object (&session->priv->using_bearer_auth);
session->priv->using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
} else if (session->priv->using_bearer_auth) {
/* This can mean the bearer auth expired, then a Basic auth is used by the libsoup;
that's not meant to be done here, thus fail early. */
g_mutex_unlock (&session->priv->property_lock);
return FALSE;
}
if (retrying && !session->priv->auth_prefilled) {
g_mutex_unlock (&session->priv->property_lock);
return FALSE;
}
session->priv->auth_prefilled = FALSE;
g_mutex_unlock (&session->priv->property_lock);
if (session->priv->using_bearer_auth) {
GError *local_error = NULL;
e_soup_session_setup_bearer_auth (session, message, TRUE, E_SOUP_AUTH_BEARER (auth), NULL, &local_error);
if (local_error) {
g_mutex_lock (&session->priv->property_lock);
/* Warn about an unclaimed error before we clear it.
* This is just to verify the errors we set here are
* actually making it back to the user. */
g_warn_if_fail (session->priv->bearer_auth_error == NULL);
g_clear_error (&session->priv->bearer_auth_error);
g_propagate_error (&session->priv->bearer_auth_error, local_error);
g_mutex_unlock (&session->priv->property_lock);
}
return FALSE;
}
credentials = e_soup_session_dup_credentials (session);
username = credentials ? e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME) : NULL;
if ((!username || !*username) && session->priv->source &&
e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
ESourceAuthentication *auth_extension;
auth_extension = e_source_get_extension (session->priv->source, E_SOURCE_EXTENSION_AUTHENTICATION);
auth_user = e_source_authentication_dup_user (auth_extension);
username = auth_user;
}
if (username && *username && credentials &&
e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD)) {
soup_auth_authenticate (auth, username, e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD));
} else if (g_strcmp0 (soup_auth_get_scheme_name (auth), "NTLM") == 0) {
soup_auth_cancel (auth);
}
e_named_parameters_free (credentials);
g_free (auth_user);
return FALSE;
}
static void
e_soup_session_set_source (ESoupSession *session,
ESource *source)
{
g_return_if_fail (E_IS_SOUP_SESSION (session));
if (source)
g_return_if_fail (E_IS_SOURCE (source));
g_return_if_fail (!session->priv->source);
session->priv->source = source ? g_object_ref (source) : NULL;
}
static void
e_soup_session_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
switch (property_id) {
case PROP_SOURCE:
e_soup_session_set_source (
E_SOUP_SESSION (object),
g_value_get_object (value));
return;
case PROP_CREDENTIALS:
e_soup_session_set_credentials (
E_SOUP_SESSION (object),
g_value_get_boxed (value));
return;
case PROP_FORCE_HTTP1:
e_soup_session_set_force_http1 (
E_SOUP_SESSION (object),
g_value_get_boolean (value));
return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
static void
e_soup_session_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
switch (property_id) {
case PROP_SOURCE:
g_value_set_object (
value,
e_soup_session_get_source (
E_SOUP_SESSION (object)));
return;
case PROP_CREDENTIALS:
g_value_take_boxed (
value,
e_soup_session_dup_credentials (
E_SOUP_SESSION (object)));
return;
case PROP_FORCE_HTTP1:
g_value_set_boolean (
value,
e_soup_session_get_force_http1 (
E_SOUP_SESSION (object)));
return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
static void
e_soup_session_finalize (GObject *object)
{
ESoupSession *session = E_SOUP_SESSION (object);
g_clear_error (&session->priv->bearer_auth_error);
g_clear_object (&session->priv->source);
g_clear_object (&session->priv->using_bearer_auth);
g_clear_pointer (&session->priv->credentials, e_named_parameters_free);
g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
g_clear_pointer (&session->priv->using_auths, g_hash_table_unref);
g_mutex_clear (&session->priv->property_lock);
g_rec_mutex_clear (&session->priv->session_lock);
/* Chain up to parent's method. */
G_OBJECT_CLASS (e_soup_session_parent_class)->finalize (object);
}
static void
e_soup_session_class_init (ESoupSessionClass *klass)
{
GObjectClass *object_class;
object_class = G_OBJECT_CLASS (klass);
object_class->set_property = e_soup_session_set_property;
object_class->get_property = e_soup_session_get_property;
object_class->finalize = e_soup_session_finalize;
/**
* ESoupSession:source:
*
* The #ESource being used for this soup session.
*
* Since: 3.26
**/
g_object_class_install_property (
object_class,
PROP_SOURCE,
g_param_spec_object (
"source",
"Source",
NULL,
E_TYPE_SOURCE,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
/**
* ESoupSession:credentials:
*
* The #ENamedParameters containing login credentials.
*
* Since: 3.26
**/
g_object_class_install_property (
object_class,
PROP_CREDENTIALS,
g_param_spec_boxed (
"credentials",
"Credentials",
NULL,
E_TYPE_NAMED_PARAMETERS,
G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY |
G_PARAM_STATIC_STRINGS));
/**
* ESoupSession:force-http1:
*
* Whether the messages created by the session should force use
* of HTTP/1 instead of trying HTTP/2 first and fallback to the HTTP/1
* when the newer version failed to connect.
*
* See e_soup_session_set_force_http1() for more information about the limitations.
*
* Since: 3.48
**/
g_object_class_install_property (
object_class,
PROP_FORCE_HTTP1,
g_param_spec_boolean (
"force-http1",
"Force HTTP/1",
NULL,
FALSE,
G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY |
G_PARAM_STATIC_STRINGS));
}
static void
e_soup_session_init (ESoupSession *session)
{
session->priv = e_soup_session_get_instance_private (session);
session->priv->using_auths = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
session->priv->ssl_info_set = FALSE;
session->priv->log_level = SOUP_LOGGER_LOG_NONE;
session->priv->auth_prefilled = FALSE;
g_mutex_init (&session->priv->property_lock);
g_rec_mutex_init (&session->priv->session_lock);
g_object_set (
G_OBJECT (session),
"timeout", 90,
"accept-language-auto", TRUE,
NULL);
if (!soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_CONTENT_DECODER))
soup_session_add_feature_by_type (SOUP_SESSION (session), SOUP_TYPE_CONTENT_DECODER);
}
/**
* e_soup_session_new:
* @source: an #ESource
*
* Creates a new #ESoupSession associated with given @source.
* The @source can be used to store and read SSL trust settings, but only if
* it already contains an #ESourceWebdav extension. Otherwise the SSL trust
* settings are ignored.
*
* Returns: (transfer full): a new #ESoupSession; free it with g_object_unref(),
* when no longer needed.
*
* Since: 3.26
**/
ESoupSession *
e_soup_session_new (ESource *source)
{
g_return_val_if_fail (E_IS_SOURCE (source), NULL);
return g_object_new (E_TYPE_SOUP_SESSION,
"source", source,
NULL);
}
/**
* e_soup_session_setup_logging:
* @session: an #ESoupSession
* @logging_level: (nullable): logging level to setup, or %NULL
*
* Setups logging for the @session. The @logging_level can be one of:
* "all" - log whole raw communication;
* "body" - the same as "all";
* "headers" - log the headers only;
* "min" - minimal logging;
* "1" - the same as "all".
* Any other value, including %NULL, disables logging.
*
* Use e_soup_session_get_log_level() to get current log level.
*
* Since: 3.26
**/
void
e_soup_session_setup_logging (ESoupSession *session,
const gchar *logging_level)
{
SoupLogger *logger;
g_return_if_fail (E_IS_SOUP_SESSION (session));
g_rec_mutex_lock (&session->priv->session_lock);
soup_session_remove_feature_by_type (SOUP_SESSION (session), SOUP_TYPE_LOGGER);
session->priv->log_level = SOUP_LOGGER_LOG_NONE;
if (!logging_level) {
g_rec_mutex_unlock (&session->priv->session_lock);
return;
}
if (g_ascii_strcasecmp (logging_level, "all") == 0 ||
g_ascii_strcasecmp (logging_level, "body") == 0 ||
g_ascii_strcasecmp (logging_level, "1") == 0)
session->priv->log_level = SOUP_LOGGER_LOG_BODY;
else if (g_ascii_strcasecmp (logging_level, "headers") == 0)
session->priv->log_level = SOUP_LOGGER_LOG_HEADERS;
else if (g_ascii_strcasecmp (logging_level, "min") == 0)
session->priv->log_level = SOUP_LOGGER_LOG_MINIMAL;
else {
g_rec_mutex_unlock (&session->priv->session_lock);
return;
}
logger = soup_logger_new (session->priv->log_level);
soup_session_add_feature (SOUP_SESSION (session), SOUP_SESSION_FEATURE (logger));
g_object_unref (logger);
g_rec_mutex_unlock (&session->priv->session_lock);
}
/**
* e_soup_session_get_log_level:
* @session: an #ESoupSession
*
* Returns: Current log level, as #SoupLoggerLogLevel
*
* Since: 3.26
**/
SoupLoggerLogLevel
e_soup_session_get_log_level (ESoupSession *session)
{
g_return_val_if_fail (E_IS_SOUP_SESSION (session), SOUP_LOGGER_LOG_NONE);
return session->priv->log_level;
}
/**
* e_soup_session_get_source:
* @session: an #ESoupSession
*
* Returns an #ESource associated with the @session, if such was set in the creation time.
*
* Returns: (transfer none) (nullable): Associated #ESource with the @session, or %NULL.
*
* Since: 3.26
**/
ESource *
e_soup_session_get_source (ESoupSession *session)
{
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
return session->priv->source;
}
/**
* e_soup_session_set_credentials:
* @session: an #ESoupSession
* @credentials: (nullable): an #ENamedParameters with credentials to use, or %NULL
*
* Sets credentials to use for connection. Using %NULL for @credentials
* unsets previous value.
*
* Since: 3.26
**/
void
e_soup_session_set_credentials (ESoupSession *session,
const ENamedParameters *credentials)
{
SoupSessionFeature *feature;
g_return_if_fail (E_IS_SOUP_SESSION (session));
g_mutex_lock (&session->priv->property_lock);
if (e_named_parameters_equal (credentials, session->priv->credentials)) {
g_mutex_unlock (&session->priv->property_lock);
return;
}
e_named_parameters_free (session->priv->credentials);
if (credentials)
session->priv->credentials = e_named_parameters_new_clone (credentials);
else
session->priv->credentials = NULL;
g_mutex_unlock (&session->priv->property_lock);
g_object_notify (G_OBJECT (session), "credentials");
/* Update also internal SoupSession state */
g_rec_mutex_lock (&session->priv->session_lock);
feature = soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_AUTH_MANAGER);
soup_auth_manager_clear_cached_credentials (SOUP_AUTH_MANAGER (feature));
g_hash_table_remove_all (session->priv->using_auths);
g_rec_mutex_unlock (&session->priv->session_lock);
}
/**
* e_soup_session_dup_credentials:
* @session: an #ESoupSession
*
* Returns: (nullable) (transfer full): A copy of the credentials being
* previously set with e_soup_session_set_credentials(), or %NULL when
* none are set. Free the returned pointer with e_named_parameters_free(),
* when no longer needed.
*
* Since: 3.26
**/
ENamedParameters *
e_soup_session_dup_credentials (ESoupSession *session)
{
ENamedParameters *credentials;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
g_mutex_lock (&session->priv->property_lock);
if (session->priv->credentials)
credentials = e_named_parameters_new_clone (session->priv->credentials);
else
credentials = NULL;
g_mutex_unlock (&session->priv->property_lock);
return credentials;
}
/**
* e_soup_session_set_force_http1:
* @session: an #ESoupSession
* @force_http1: the value to set
*
* Sets whether the messages created through the @session using
* e_soup_session_new_message() or e_soup_session_new_message_from_uri()
* should force use of the HTTP/1, instead of trying HTTP/2 and fallback to HTTP/1,
* when the newer version cannot be used.
*
* The property has no effect when e_soup_session_util_get_force_http1_supported()
* returns %FALSE.
*
* Since: 3.48
**/
void
e_soup_session_set_force_http1 (ESoupSession *session,
gboolean force_http1)
{
g_return_if_fail (E_IS_SOUP_SESSION (session));
#ifdef HAVE_SOUP_MESSAGE_SET_FORCE_HTTP1
if ((session->priv->force_http1 ? 1 : 0) == (force_http1 ? 1 : 0))
return;
session->priv->force_http1 = force_http1;
g_object_notify (G_OBJECT (session), "force-http1");
#endif
}
/**
* e_soup_session_get_force_http1:
* @session: an #ESoupSession
*
* Returns whether it's forced to use HTTP/1 for the messages
* created by the @session. See e_soup_session_set_force_http1()
* for more information about the limitations.
*
* Returns: whether it's forced to use HTTP/1
*
* Since: 3.48
**/
gboolean
e_soup_session_get_force_http1 (ESoupSession *session)
{
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
return session->priv->force_http1;
}
/**
* e_soup_session_get_authentication_requires_credentials:
* @session: an #ESoupSession
*
* Returns: Whether the last connection attempt required any credentials.
* Authentications like OAuth2 do not want extra credentials to work.
*
* Since: 3.28
**/
gboolean
e_soup_session_get_authentication_requires_credentials (ESoupSession *session)
{
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
return !session->priv->using_bearer_auth;
}
/**
* e_soup_session_get_ssl_error_details:
* @session: an #ESoupSession
* @out_certificate_pem: (out) (optional): return location for a server TLS/SSL certificate
* in PEM format, when the last operation failed with a TLS/SSL error, or %NULL
* @out_certificate_errors: (out) (optional): return location for a #GTlsCertificateFlags,
* with certificate error flags when the operation failed with a TLS/SSL error, or %NULL
*
* Populates @out_certificate_pem and @out_certificate_errors with the last values
* returned on #G_TLS_ERROR_BAD_CERTIFICATE error.
*
* Returns: Whether the information was available and set to the out parameters.
*
* Since: 3.26
**/
gboolean
e_soup_session_get_ssl_error_details (ESoupSession *session,
gchar **out_certificate_pem,
GTlsCertificateFlags *out_certificate_errors)
{
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_mutex_lock (&session->priv->property_lock);
if (!session->priv->ssl_info_set) {
g_mutex_unlock (&session->priv->property_lock);
return FALSE;
}
if (out_certificate_pem)
*out_certificate_pem = g_strdup (session->priv->ssl_certificate_pem);
if (out_certificate_errors)
*out_certificate_errors = session->priv->ssl_certificate_errors;
g_mutex_unlock (&session->priv->property_lock);
return TRUE;
}
/**
* e_soup_session_handle_authentication_failure:
* @session: an #ESoupSession
* @credentials: (nullable): credentials used for the authentication
* @op_error: a #GError of the authentication operation
* @out_auth_result: (out): an #ESourceAuthenticationResult with an authentication result
* @out_certificate_pem: (out) (optional): return location for a server TLS/SSL certificate
* in PEM format, when the last operation failed with a TLS/SSL error, or %NULL
* @out_certificate_errors: (out) (optional): return location for a #GTlsCertificateFlags,
* with certificate error flags when the operation failed with a TLS/SSL error, or %NULL
* @error: return location for a #GError, or %NULL
*
* Handles authentication failure and sets appropriate value to the @out_auth_result
* for the provided @op_error and used @credentials. Converts the @op_error
* into an appropriate error returned in the @error.
*
* Also updates connection status on the associated #ESource with the @session.
*
* Since: 3.46
**/
void
e_soup_session_handle_authentication_failure (ESoupSession *session,
const ENamedParameters *credentials,
const GError *op_error,
ESourceAuthenticationResult *out_auth_result,
gchar **out_certificate_pem,
GTlsCertificateFlags *out_certificate_errors,
GError **error)
{
ESource *source;
gboolean requires_credentials;
gboolean credentials_empty;
gboolean is_tls_error;
g_return_if_fail (E_IS_SOUP_SESSION (session));
g_return_if_fail (out_auth_result != NULL);
source = e_soup_session_get_source (session);
requires_credentials = e_soup_session_get_authentication_requires_credentials (session);
credentials_empty = (!credentials || !e_named_parameters_count (credentials) ||
(e_named_parameters_count (credentials) == 1 && e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_SSL_TRUST))) &&
requires_credentials;
is_tls_error = g_error_matches (op_error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE);
*out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
if (g_error_matches (op_error, E_SOUP_SESSION_ERROR, SOUP_STATUS_FORBIDDEN) && credentials_empty) {
*out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
} else if (g_error_matches (op_error, E_SOUP_SESSION_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
if (credentials_empty)
*out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
else
*out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
} else if (g_error_matches (op_error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) ||
(!requires_credentials && g_error_matches (op_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))) {
*out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
} else if (!op_error) {
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Unknown error"));
}
if (op_error)
g_propagate_error (error, g_error_copy (op_error));
if (is_tls_error) {
*out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
if (source)
e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
e_soup_session_get_ssl_error_details (session, out_certificate_pem, out_certificate_errors);
} else if (source) {
e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
}
}
static void
e_soup_session_preset_message (SoupMessage *message)
{
if (message) {
GUri *normalized_uri;
normalized_uri = e_soup_session_util_normalize_uri_path (soup_message_get_uri (message));
if (normalized_uri) {
soup_message_set_uri (message, normalized_uri);
g_uri_unref (normalized_uri);
}
soup_message_headers_append (soup_message_get_request_headers (message), "User-Agent", "Evolution/" VERSION);
soup_message_headers_append (soup_message_get_request_headers (message), "Connection", "close");
/* Disable caching for proxies (RFC 4918, section 10.4.5) */
soup_message_headers_append (soup_message_get_request_headers (message), "Cache-Control", "no-cache");
soup_message_headers_append (soup_message_get_request_headers (message), "Pragma", "no-cache");
}
}
/**
* e_soup_session_new_message:
* @session: an #ESoupSession
* @method: an HTTP method
* @uri_string: a URI string to use for the request
* @error: return location for a #GError, or %NULL
*
* Creates a new #SoupMessage, similar to soup_message_new(),
* but also presets request headers with "User-Agent" to be "Evolution/version"
* and with "Connection" to be "close".
*
* See also e_soup_session_new_message_from_uri().
*
* Returns: (transfer full): a new #SoupMessage, or %NULL on error
*
* Since: 3.26
**/
SoupMessage *
e_soup_session_new_message (ESoupSession *session,
const gchar *method,
const gchar *uri_string,
GError **error)
{
SoupMessage *message;
GUri *uri;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
uri = g_uri_parse (uri_string, SOUP_HTTP_URI_FLAGS | G_URI_FLAGS_PARSE_RELAXED, error);
if (!uri)
return NULL;
message = e_soup_session_new_message_from_uri (session, method, uri, error);
g_uri_unref (uri);
return message;
}
/**
* e_soup_session_new_message_from_uri:
* @session: an #ESoupSession
* @method: an HTTP method
* @uri: a #GUri to use for the request
* @error: return location for a #GError, or %NULL
*
* Creates a new #SoupMessage, similar to soup_message_new_from_uri(),
* but also presets request headers with "User-Agent" to be "Evolution/version"
* and with "Connection" to be "close".
*
* See also e_soup_session_new_message().
*
* Returns: (transfer full): a new #SoupMessage, or %NULL on error
*
* Since: 3.46
**/
SoupMessage *
e_soup_session_new_message_from_uri (ESoupSession *session,
const gchar *method,
GUri *uri,
GError **error)
{
SoupMessage *message;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
if (g_uri_get_user (uri) && !g_uri_get_password (uri)) {
/* Do not allow setting user without password in the URI, because libsoup3 tries
to authenticate even without password, which can break the code. */
GUri *uri_copy;
uri_copy = soup_uri_copy (uri,
SOUP_URI_USER, NULL,
SOUP_URI_PASSWORD, NULL,
SOUP_URI_NONE);
message = soup_message_new_from_uri (method, uri_copy);
g_uri_unref (uri_copy);
} else {
message = soup_message_new_from_uri (method, uri);
}
if (!message)
return NULL;
e_soup_session_preset_message (message);
#ifdef HAVE_SOUP_MESSAGE_SET_FORCE_HTTP1
/* only set to TRUE, do not touch it otherwise */
if (session->priv->force_http1)
soup_message_set_force_http1 (message, TRUE);
#endif
return message;
}
static gboolean
e_soup_session_extract_ssl_data (ESoupSession *session,
SoupMessage *message,
gchar **out_certificate_pem,
GTlsCertificateFlags *out_certificate_errors)
{
GTlsCertificate *certificate = NULL;
gboolean res = FALSE;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
if (!out_certificate_pem)
return FALSE;
g_object_get (G_OBJECT (message),
"tls-peer-certificate", &certificate,
out_certificate_errors ? "tls-peer-certificate-errors" : NULL, out_certificate_errors,
NULL);
if (certificate) {
g_object_get (certificate, "certificate-pem", out_certificate_pem, NULL);
res = TRUE;
g_object_unref (certificate);
}
return res;
}
static void
e_soup_session_extract_ssl_data_internal (ESoupSession *session,
SoupMessage *message)
{
g_return_if_fail (E_IS_SOUP_SESSION (session));
g_return_if_fail (SOUP_IS_MESSAGE (message));
g_mutex_lock (&session->priv->property_lock);
g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
session->priv->ssl_info_set = e_soup_session_extract_ssl_data (session, message,
&session->priv->ssl_certificate_pem, &session->priv->ssl_certificate_errors);
g_mutex_unlock (&session->priv->property_lock);
}
static gboolean
e_soup_session_extract_google_daily_limit_error (gconstpointer read_bytes,
gsize bytes_length,
GError **error)
{
gchar *body;
gboolean contains_daily_limit = FALSE;
if (!read_bytes || !bytes_length)
return FALSE;
body = g_strndup (read_bytes, bytes_length);
/* Do not localize this string, it is returned by the server. */
if (body && (e_util_strstrcase (body, "Daily Limit") ||
e_util_strstrcase (body, "https://console.developers.google.com/"))) {
/* Special-case this condition and provide this error up to the UI. */
g_set_error_literal (error, E_SOUP_SESSION_ERROR, SOUP_STATUS_FORBIDDEN, body);
contains_daily_limit = TRUE;
}
g_free (body);
return contains_daily_limit;
}
/**
* e_soup_session_check_result:
* @session: an #ESoupSession
* @message: a #SoupMessage
* @read_bytes: (nullable): optional bytes which had been read from the stream, or %NULL
* @bytes_length: how many bytes had been read; ignored when @read_bytes is %NULL
* @error: return location for a #GError, or %NULL
*
* Checks result of the @message and sets the @error if it failed.
* When it failed and the @read_bytes is provided, then these are
* set to @message's response body, thus it can be used later.
*
* Returns: Whether succeeded, aka %TRUE, when no error recognized
* and %FALSE otherwise.
*
* Since: 3.26
**/
gboolean
e_soup_session_check_result (ESoupSession *session,
SoupMessage *message,
gconstpointer read_bytes,
gsize bytes_length,
GError **error)
{
gboolean success;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
success = SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message));
if (!success) {
if (soup_message_get_status (message) == SOUP_STATUS_FORBIDDEN &&
e_soup_session_extract_google_daily_limit_error (read_bytes, bytes_length, error)) {
/* Nothing to do */
} else {
g_set_error (error, E_SOUP_SESSION_ERROR, soup_message_get_status (message),
_("Failed with HTTP error %d: %s"), soup_message_get_status (message),
e_soup_session_util_status_to_string (soup_message_get_status (message),
soup_message_get_reason_phrase (message)));
}
e_soup_session_extract_ssl_data_internal (session, message);
}
return success;
}
static void
e_soup_session_restore_method_on_restarted_cb (SoupMessage *message,
gpointer user_data)
{
const gchar *orig_method = user_data;
g_return_if_fail (orig_method != NULL);
/* Redirect can change the method, this makes sure it'll be preserved. */
if (g_strcmp0 (orig_method, soup_message_get_method (message)) != 0)
soup_message_set_method (message, orig_method);
}
static gboolean
e_soup_session_prepare_message_send_phase1_sync (ESoupSession *session,
SoupMessage *message,
gulong *out_authenticate_id,
gulong *out_restarted_id,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
if (!e_soup_session_maybe_prepare_auth (session, message, cancellable, error))
return FALSE;
*out_authenticate_id = g_signal_connect (message, "authenticate",
G_CALLBACK (e_soup_session_authenticate_cb), session);
*out_restarted_id = g_signal_connect_data (message, "restarted",
G_CALLBACK (e_soup_session_restore_method_on_restarted_cb), g_strdup (soup_message_get_method (message)), (GClosureNotify) g_free, 0);
/* Always connect the SSL trust, even when the WebDAV extension is not present on the source,
otherwise any SSL trust is not properly handled. */
if (session->priv->source)
e_soup_ssl_trust_connect (message, session->priv->source);
return TRUE;
}
static gboolean
e_soup_session_prepare_message_send_phase2_sync (ESoupSession *session,
SoupMessage *message,
GCancellable *cancellable,
GError **error)
{
ESoupAuthBearer *using_bearer_auth = NULL;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
g_mutex_lock (&session->priv->property_lock);
if (session->priv->using_bearer_auth)
using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
g_mutex_unlock (&session->priv->property_lock);
if (using_bearer_auth &&
e_soup_auth_bearer_is_expired (using_bearer_auth)) {
GError *local_error = NULL;
if (!e_soup_session_setup_bearer_auth (session, message, FALSE, using_bearer_auth, cancellable, &local_error)) {
if (local_error) {
g_propagate_error (error, local_error);
} else {
g_set_error_literal (&local_error, E_SOUP_SESSION_ERROR, SOUP_STATUS_BAD_REQUEST, _("Failed to setup authentication"));
}
g_object_unref (using_bearer_auth);
return FALSE;
}
}
g_clear_object (&using_bearer_auth);
return TRUE;
}
typedef struct _AsyncSendData {
guint size;
ESoupSession *session;
GTask *task;
gulong authenticate_id;
gulong restarted_id;
gchar *certificate_pem;
GTlsCertificateFlags certificate_errors;
gint io_priority;
gboolean caught_bearer_expired;
} AsyncSendData;
static void
async_send_data_free (gpointer ptr)
{
AsyncSendData *asd = ptr;
if (asd) {
/* The ads->task is unreffed in e_soup_session_send_message_ready_cb() */
g_clear_object (&asd->session);
g_free (asd->certificate_pem);
g_slice_free (AsyncSendData, asd);
}
}
/**
* e_soup_session_prepare_message_send_sync:
* @session: an #ESoupSession
* @message: a #SoupMessage to prepare for asynchronous send
* @cancellable: optional #GCancellable object, or %NULL
* @error: return location for a #GError, or %NULL
*
* Prepares the @message to be a sent asynchronously with
* e_soup_session_send_message(). The returned pointer is passed
* to the e_soup_session_send_message() as the prepare_data
* parameter.
*
* Returns: (nullable) (transfer full): prepare data for e_soup_session_send_message(),
* or %NULL on error.
*
* Since: 3.46
**/
gpointer
e_soup_session_prepare_message_send_sync (ESoupSession *session,
SoupMessage *message,
GCancellable *cancellable,
GError **error)
{
gulong authenticate_id = 0;
gulong restarted_id = 0;
gboolean success;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), NULL);
success = e_soup_session_prepare_message_send_phase1_sync (session, message, &authenticate_id, &restarted_id, cancellable, error);
if (success) {
success = e_soup_session_prepare_message_send_phase2_sync (session, message, cancellable, error);
if (!success) {
if (authenticate_id)
g_signal_handler_disconnect (message, authenticate_id);
if (restarted_id)
g_signal_handler_disconnect (message, restarted_id);
}
}
if (success) {
AsyncSendData *asd;
asd = g_slice_new0 (AsyncSendData);
asd->size = sizeof (AsyncSendData);
asd->authenticate_id = authenticate_id;
asd->restarted_id = restarted_id;
return asd;
}
return NULL;
}
static GByteArray *
e_soup_session_read_bytes (SoupMessage *message,
GInputStream *input_stream,
GCancellable *cancellable,
GError **error)
{
GByteArray *bytes;
goffset expected_length;
gpointer buffer;
gsize nread = 0;
gboolean success = FALSE;
expected_length = soup_message_headers_get_content_length (soup_message_get_response_headers (message));
if (expected_length > 0)
bytes = g_byte_array_sized_new (expected_length > 1024 * 1024 * 10 ? 1024 * 1024 * 10 : expected_length);
else
bytes = g_byte_array_new ();
buffer = g_malloc (BUFFER_SIZE);
while (success = g_input_stream_read_all (input_stream, buffer, BUFFER_SIZE, &nread, cancellable, error),
success && nread > 0) {
g_byte_array_append (bytes, buffer, nread);
}
g_free (buffer);
if (!success)
g_clear_pointer (&bytes, g_byte_array_unref);
return bytes;
}
static void
e_soup_session_store_data_on_message (SoupMessage *message,
GInputStream *input_stream,
GCancellable *cancellable)
{
if (input_stream) {
GByteArray *bytes;
bytes = e_soup_session_read_bytes (message, input_stream, cancellable, NULL);
if (bytes) {
g_object_set_data_full (G_OBJECT (message), E_SOUP_SESSION_MESSAGE_BYTES_KEY,
bytes, (GDestroyNotify) g_byte_array_unref);
}
}
}
static void
e_soup_session_send_message_ready_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
AsyncSendData *asd = user_data;
ESoupSession *esession;
SoupSession *session;
SoupMessage *message;
GInputStream *input_stream;
GError *local_error = NULL;
gboolean caught_bearer_expired = FALSE;
g_return_if_fail (asd != NULL);
session = SOUP_SESSION (source_object);
esession = E_SOUP_SESSION (session);
g_rec_mutex_lock (&esession->priv->session_lock);
input_stream = soup_session_send_finish (session, result, &local_error);
message = soup_session_get_async_result_message (session, result);
if (message) {
if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) {
e_soup_session_store_data_on_message (message, input_stream, NULL);
g_clear_object (&input_stream);
}
if (g_error_matches (local_error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE)) {
e_soup_session_extract_ssl_data (E_SOUP_SESSION (session), message, &asd->certificate_pem, &asd->certificate_errors);
} else if (!local_error && !SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) {
GByteArray *bytes = e_soup_session_util_get_message_bytes (message);
if (soup_message_get_status (message) != SOUP_STATUS_FORBIDDEN ||
!e_soup_session_extract_google_daily_limit_error (bytes ? bytes->data : NULL, bytes ? bytes->len : 0, &local_error))
g_set_error_literal (&local_error, E_SOUP_SESSION_ERROR, soup_message_get_status (message),
soup_message_get_reason_phrase (message));
}
}
g_rec_mutex_unlock (&esession->priv->session_lock);
if (message && soup_message_get_status (message) == SOUP_STATUS_UNAUTHORIZED && !asd->caught_bearer_expired) {
g_mutex_lock (&esession->priv->property_lock);
if (esession->priv->using_bearer_auth && e_soup_auth_bearer_is_expired (esession->priv->using_bearer_auth)) {
g_signal_emit_by_name (message, "restarted");
asd->caught_bearer_expired = TRUE;
caught_bearer_expired = TRUE;
}
g_mutex_unlock (&esession->priv->property_lock);
}
if (caught_bearer_expired) {
g_clear_error (&local_error);
g_clear_object (&input_stream);
g_rec_mutex_lock (&esession->priv->session_lock);
soup_session_send_async (session, message, asd->io_priority, g_task_get_cancellable (asd->task),
e_soup_session_send_message_ready_cb, asd);
g_rec_mutex_unlock (&esession->priv->session_lock);
} else {
if (local_error) {
g_task_return_error (asd->task, local_error);
g_clear_object (&input_stream);
} else {
g_task_return_pointer (asd->task, input_stream, g_object_unref);
}
g_object_unref (asd->task);
}
}
/**
* e_soup_session_send_message:
* @session: an #ESoupSession
* @message: a #SoupMessage to send
* @io_priority: the I/O priority of the request, like %G_PRIORITY_DEFAULT
* @prepare_data: (transfer full): data returned from e_soup_session_prepare_message_send_sync()
* @cancellable: optional #GCancellable object, or %NULL
* @callback: (scope async): the callback to invoke once the request is finished
* @user_data: user data for @callback
*
* Asynchronously sends the @message. Finish the call with
* e_soup_session_send_message_finish().
*
* The @prepare_data is a result of the e_soup_session_prepare_message_send_sync()
* and this function assumes ownership of it. The @prepare_data cannot be used
* again after this call.
*
* Since: 3.46
**/
void
e_soup_session_send_message (ESoupSession *session,
SoupMessage *message,
gint io_priority,
gpointer prepare_data,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
AsyncSendData *asd = prepare_data;
g_return_if_fail (E_IS_SOUP_SESSION (session));
g_return_if_fail (SOUP_IS_MESSAGE (message));
g_return_if_fail (prepare_data != NULL);
g_return_if_fail (asd->size == sizeof (AsyncSendData));
g_return_if_fail (asd->session == NULL);
g_return_if_fail (asd->task == NULL);
asd->session = g_object_ref (session);
asd->task = g_task_new (session, cancellable, callback, user_data);
asd->caught_bearer_expired = FALSE;
asd->io_priority = io_priority;
g_task_set_source_tag (asd->task, e_soup_session_send_message);
g_task_set_task_data (asd->task, asd, async_send_data_free);
g_rec_mutex_lock (&session->priv->session_lock);
soup_session_send_async (SOUP_SESSION (session), message, io_priority, cancellable,
e_soup_session_send_message_ready_cb, asd);
g_rec_mutex_unlock (&session->priv->session_lock);
}
/**
* e_soup_session_send_message_finish:
* @session: an #ESoupSession
* @result: a #GAsyncResult object
* @out_certificate_pem: (out) (optional) (nullable): return location for a server TLS/SSL certificate
* in PEM format, when the last operation failed with a TLS/SSL error
* @out_certificate_errors: (out) (optional): return location for a #GTlsCertificateFlags,
* with certificate error flags when the operation failed with a TLS/SSL error
* @error: return location for a #GError, or %NULL
*
* Finishes the call of e_soup_session_send_message(). This is supposed to
* be called from the callback passed to the e_soup_session_send_message().
*
* The optional @out_certificate_pem and @out_certificate_errors are set,
* when provided, only if the operation failed with a TLS/SSL error.
*
* Make sure the #GInputStream is read and freed from the same thread,
* and with the same thread default main context, which this function
* was called from, otherwise it can break libsoup3.
*
* Returns: (transfer full) (nullable): a #GInputStream for reading the response body, or %NULL on error
*
* Since: 3.46
**/
GInputStream *
e_soup_session_send_message_finish (ESoupSession *session,
GAsyncResult *result,
gchar **out_certificate_pem,
GTlsCertificateFlags *out_certificate_errors,
GError **error)
{
GInputStream *input_stream;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
input_stream = g_task_propagate_pointer (G_TASK (result), error);
if (!input_stream) {
AsyncSendData *asd = g_task_get_task_data (G_TASK (result));
if (out_certificate_pem)
*out_certificate_pem = asd ? g_steal_pointer (&asd->certificate_pem) : NULL;
if (out_certificate_errors)
*out_certificate_errors = asd ? asd->certificate_errors : 0;
}
return input_stream;
}
/**
* e_soup_session_send_message_sync:
* @session: an #ESoupSession
* @message: a #SoupMessage to send
* @cancellable: optional #GCancellable object, or %NULL
* @error: return location for a #GError, or %NULL
*
* Synchronously sends prepared message and returns #GInputStream
* that can be used to read its contents.
*
* This calls soup_session_send() internally, but it also setups
* the @message according to #ESoupSession:source authentication
* settings. It also extracts information about used certificate,
* in case of G_TLS_ERROR_BAD_CERTIFICATE error and keeps it
* for later use by e_soup_session_get_ssl_error_details().
*
* Use e_soup_session_send_message_simple_sync() to read whole
* content into a #GByteArray.
*
* Note that SoupSession doesn't log content read from GInputStream,
* thus the caller may print the read content on its own when needed.
*
* Note the @message is fully filled only after there is anything
* read from the resulting #GInputStream, thus use
* e_soup_session_check_result() to verify that the receive had
* been finished properly.
*
* Make sure the #GInputStream is read and freed from the same thread,
* and with the same thread default main context, which this function
* was called from, otherwise it can break libsoup3.
*
* Returns: (transfer full): A newly allocated #GInputStream,
* that can be used to read from the URI pointed to by @message.
* Free it with g_object_unref(), when no longer needed.
*
* Since: 3.26
**/
GInputStream *
e_soup_session_send_message_sync (ESoupSession *session,
SoupMessage *message,
GCancellable *cancellable,
GError **error)
{
GInputStream *input_stream;
gboolean redirected;
gboolean caught_bearer_expired = FALSE;
gint resend_count = 0;
gulong authenticate_id = 0;
gulong restarted_id = 0;
GError *local_error = NULL;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), NULL);
if (!e_soup_session_prepare_message_send_phase1_sync (session, message, &authenticate_id, &restarted_id, cancellable, error))
return NULL;
g_mutex_lock (&session->priv->property_lock);
g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
session->priv->ssl_certificate_errors = 0;
session->priv->ssl_info_set = FALSE;
g_mutex_unlock (&session->priv->property_lock);
redirected = TRUE;
while (redirected) {
redirected = FALSE;
if (!e_soup_session_prepare_message_send_phase2_sync (session, message, cancellable, error)) {
if (authenticate_id)
g_signal_handler_disconnect (message, authenticate_id);
if (restarted_id)
g_signal_handler_disconnect (message, restarted_id);
return NULL;
}
g_rec_mutex_lock (&session->priv->session_lock);
input_stream = soup_session_send (SOUP_SESSION (session), message, cancellable, &local_error);
g_rec_mutex_unlock (&session->priv->session_lock);
if (input_stream) {
if (SOUP_STATUS_IS_REDIRECTION (soup_message_get_status (message))) {
/* libsoup uses 20, but the constant is not in any public header */
if (resend_count >= 30) {
g_set_error_literal (&local_error, SOUP_SESSION_ERROR, SOUP_SESSION_ERROR_TOO_MANY_REDIRECTS, _("Too many redirects"));
g_clear_object (&input_stream);
} else {
const gchar *new_location;
new_location = soup_message_headers_get_list (soup_message_get_response_headers (message), "Location");
if (new_location) {
GUri *new_uri;
new_uri = g_uri_parse_relative (soup_message_get_uri (message), new_location, SOUP_HTTP_URI_FLAGS | G_URI_FLAGS_PARSE_RELAXED, NULL);
soup_message_set_uri (message, new_uri);
g_clear_object (&input_stream);
g_uri_unref (new_uri);
g_signal_emit_by_name (message, "restarted");
resend_count++;
redirected = TRUE;
}
}
}
} else if (soup_message_get_status (message) == SOUP_STATUS_UNAUTHORIZED && !caught_bearer_expired) {
g_mutex_lock (&session->priv->property_lock);
if (session->priv->using_bearer_auth && e_soup_auth_bearer_is_expired (session->priv->using_bearer_auth)) {
g_signal_emit_by_name (message, "restarted");
resend_count++;
redirected = TRUE;
caught_bearer_expired = TRUE;
g_clear_error (&local_error);
}
g_mutex_unlock (&session->priv->property_lock);
}
}
if (authenticate_id)
g_signal_handler_disconnect (message, authenticate_id);
if (restarted_id)
g_signal_handler_disconnect (message, restarted_id);
if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) {
e_soup_session_store_data_on_message (message, input_stream, cancellable);
g_clear_object (&input_stream);
}
if (g_error_matches (local_error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE)) {
e_soup_session_extract_ssl_data_internal (session, message);
} else if (!local_error && !SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) {
GByteArray *bytes = e_soup_session_util_get_message_bytes (message);
if (soup_message_get_status (message) != SOUP_STATUS_FORBIDDEN ||
!e_soup_session_extract_google_daily_limit_error (bytes ? bytes->data : NULL, bytes ? bytes->len : 0, error))
g_set_error_literal (&local_error, E_SOUP_SESSION_ERROR, soup_message_get_status (message),
soup_message_get_reason_phrase (message));
}
if (local_error) {
g_propagate_error (error, local_error);
g_clear_object (&input_stream);
}
return input_stream;
}
/**
* e_soup_session_send_message_simple_sync:
* @session: an #ESoupSession
* @message: a #SoupMessage to send
* @cancellable: optional #GCancellable object, or %NULL
* @error: return location for a #GError, or %NULL
*
* Similar to e_soup_session_send_message_sync(), except it reads
* whole response content into memory and returns it as a #GByteArray.
* Use e_soup_session_send_message_sync() when you want to have
* more control on the content read.
*
* Returns: (transfer full): A newly allocated #GByteArray,
* which contains whole content from the URI pointed to by @message.
*
* Since: 3.26
**/
GByteArray *
e_soup_session_send_message_simple_sync (ESoupSession *session,
SoupMessage *message,
GCancellable *cancellable,
GError **error)
{
GInputStream *input_stream;
GByteArray *bytes;
gboolean success = FALSE;
g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
g_return_val_if_fail (SOUP_IS_MESSAGE (message), NULL);
input_stream = e_soup_session_send_message_sync (session, message, cancellable, error);
if (!input_stream) {
bytes = e_soup_session_util_get_message_bytes (message);
if (bytes) {
GError *local_error = NULL;
if (!e_soup_session_check_result (session, message, bytes->data, bytes->len, &local_error) && local_error) {
g_clear_error (error);
g_propagate_error (error, local_error);
}
}
return NULL;
}
bytes = e_soup_session_read_bytes (message, input_stream, cancellable, error);
g_object_unref (input_stream);
success = bytes != NULL;
if (success)
success = e_soup_session_check_result (session, message, bytes->data, bytes->len, error);
if (!success)
g_clear_pointer (&bytes, g_byte_array_unref);
return bytes;
}
/**
* e_soup_session_util_status_to_string:
* @status_code: an HTTP status code
* @reason_phrase: (nullable): preferred string to use for the message, or %NULL
*
* Returns the @reason_phrase, if it's non-%NULL and non-empty, a static string
* corresponding to @status_code. In case neither that can be found a localized
* "Unknown error" message is returned.
*
* Returns: (transfer none): Error text based on given arguments. The returned
* value is valid as long as @reason_phrase is not freed.
*
* Since: 3.26
**/
const gchar *
e_soup_session_util_status_to_string (guint status_code,
const gchar *reason_phrase)
{
if (!reason_phrase || !*reason_phrase)
reason_phrase = soup_status_get_phrase (status_code);
if (reason_phrase && *reason_phrase)
return reason_phrase;
return _("Unknown error");
}
static gboolean
part_needs_encoding (const gchar *part)
{
const gchar *pp;
if (!part || !*part)
return FALSE;
for (pp = part; *pp; pp++) {
if (!strchr ("/!()+-*~';,.$&_", *pp) &&
!g_ascii_isalnum (*pp) &&
(*pp != '%' || pp[1] != '4' || pp[2] != '0') && /* cover '%40', aka '@', as a common case, to avoid unnecessary allocations */
(*pp != '%' || pp[1] != '2' || pp[2] != '0')) { /* '%20', aka ' ' */
break;
}
}
return *pp;
}
/**
* e_soup_session_util_normalize_uri_path:
* @uri: a #GUri to normalize the path for
*
* Normalizes the path of the @uri, aka encodes characters, which should
* be encoded, if needed. Returns, modified URI when any change had been made to the path.
* It doesn't touch other parts of the @uri.
*
* Returns: (transfer full) (nullable): a new #GUri with modified path, or %NULL, when
* no change was required.
*
* Since: 3.46
**/
GUri *
e_soup_session_util_normalize_uri_path (GUri *uri)
{
const gchar *path;
gchar **parts, *tmp;
GUri *nuri = NULL;
GUriFlags flags;
gint ii;
if (!uri)
return NULL;
flags = g_uri_get_flags (uri);
if ((flags & G_URI_FLAGS_ENCODED) != 0 ||
(flags & G_URI_FLAGS_ENCODED_PATH) != 0)
return NULL;
path = g_uri_get_path (uri);
if (!*path || g_strcmp0 (path, "/") == 0)
return NULL;
if (!part_needs_encoding (path))
return NULL;
parts = g_strsplit (path, "/", -1);
if (!parts)
return NULL;
for (ii = 0; parts[ii]; ii++) {
gchar *part = parts[ii];
if (part_needs_encoding (part)) {
if (strchr (part, '%')) {
tmp = g_uri_unescape_string (part, NULL);
g_free (part);
part = tmp;
}
tmp = g_uri_escape_string (part, NULL, FALSE);
g_free (part);
parts[ii] = tmp;
}
}
tmp = g_strjoinv ("/", parts);
if (g_strcmp0 (path, tmp) != 0)
nuri = soup_uri_copy (uri, SOUP_URI_PATH, tmp, SOUP_URI_NONE);
g_free (tmp);
g_strfreev (parts);
return nuri;
}
typedef struct _EInputStreamWrapper {
GInputStream parent;
GInputStream *input_stream;
goffset read_from;
} EInputStreamWrapper;
typedef struct _EInputStreamWrapperClass {
GInputStreamClass parent_class;
} EInputStreamWrapperClass;
GType e_input_stream_wrapper_get_type (void);
static void e_input_stream_wrapper_pollable_iface_init (GPollableInputStreamInterface *iface);
G_DEFINE_TYPE_WITH_CODE (EInputStreamWrapper, e_input_stream_wrapper, G_TYPE_INPUT_STREAM,
G_IMPLEMENT_INTERFACE (G_TYPE_POLLABLE_INPUT_STREAM, e_input_stream_wrapper_pollable_iface_init))
#define E_INPUT_STREAM_WRAPPER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), e_input_stream_wrapper_get_type (), EInputStreamWrapper))
static gssize
e_input_stream_wrapper_read_fn (GInputStream *stream,
void *buffer,
gsize count,
GCancellable *cancellable,
GError **error)
{
EInputStreamWrapper *wrapper = E_INPUT_STREAM_WRAPPER (stream);
return g_input_stream_read (wrapper->input_stream, buffer, count, cancellable, error);
}
static gssize
e_input_stream_wrapper_skip (GInputStream *stream,
gsize count,
GCancellable *cancellable,
GError **error)
{
EInputStreamWrapper *wrapper = E_INPUT_STREAM_WRAPPER (stream);
return g_input_stream_skip (wrapper->input_stream, count, cancellable, error);
}
static gboolean
e_input_stream_wrapper_close_fn (GInputStream *stream,
GCancellable *cancellable,
GError **error)
{
/* Always success, but without closing the self::input_stream */
return TRUE;
}
static gboolean
e_input_stream_wrapper_is_readable (GPollableInputStream *stream)
{
return TRUE;
}
static GSource *
e_input_stream_wrapper_create_source (GPollableInputStream *stream,
GCancellable *cancellable)
{
GSource *base_source, *pollable_source;
base_source = g_timeout_source_new (0);
pollable_source = g_pollable_source_new_full (stream, base_source, cancellable);
g_source_unref (base_source);
return pollable_source;
}
static void
e_input_stream_wrapper_dispose (GObject *object)
{
EInputStreamWrapper *wrapper = E_INPUT_STREAM_WRAPPER (object);
g_clear_object (&wrapper->input_stream);
/* Chain up to parent's method. */
G_OBJECT_CLASS (e_input_stream_wrapper_parent_class)->dispose (object);
}
static void
e_input_stream_wrapper_class_init (EInputStreamWrapperClass *klass)
{
GInputStreamClass *input_stream_class;
GObjectClass *object_class;
input_stream_class = G_INPUT_STREAM_CLASS (klass);
input_stream_class->read_fn = e_input_stream_wrapper_read_fn;
input_stream_class->skip = e_input_stream_wrapper_skip;
input_stream_class->close_fn = e_input_stream_wrapper_close_fn;
object_class = G_OBJECT_CLASS (klass);
object_class->dispose = e_input_stream_wrapper_dispose;
}
static void
e_input_stream_wrapper_pollable_iface_init (GPollableInputStreamInterface *iface)
{
iface->is_readable = e_input_stream_wrapper_is_readable;
iface->create_source = e_input_stream_wrapper_create_source;
}
static void
e_input_stream_wrapper_init (EInputStreamWrapper *self)
{
}
static void
e_input_stream_wrapper_assign (EInputStreamWrapper *self,
GInputStream *input_stream)
{
self->input_stream = g_object_ref (input_stream);
if (G_IS_SEEKABLE (input_stream))
self->read_from = g_seekable_tell (G_SEEKABLE (input_stream));
}
static void
e_input_stream_wrapper_rewind (EInputStreamWrapper *self)
{
if (G_IS_SEEKABLE (self->input_stream) && self->read_from != g_seekable_tell (G_SEEKABLE (self->input_stream)))
g_seekable_seek (G_SEEKABLE (self->input_stream), self->read_from, G_SEEK_SET, NULL, NULL);
}
static GInputStream *
e_input_stream_wrapper_dup (EInputStreamWrapper *self)
{
EInputStreamWrapper *dup;
e_input_stream_wrapper_rewind (self);
dup = g_object_new (e_input_stream_wrapper_get_type (), NULL);
dup->input_stream = g_object_ref (self->input_stream);
dup->read_from = self->read_from;
return G_INPUT_STREAM (dup);
}
typedef struct _MessageData {
GInputStream *input_stream;
gssize length;
} MessageData;
static MessageData *
message_data_new (GInputStream *input_stream,
gssize length)
{
MessageData *md;
EInputStreamWrapper *wrapper;
wrapper = g_object_new (e_input_stream_wrapper_get_type (), NULL);
e_input_stream_wrapper_assign (wrapper, input_stream);
md = g_slice_new0 (MessageData);
md->input_stream = G_INPUT_STREAM (wrapper);
md->length = length;
return md;
}
static void
message_data_free (gpointer ptr)
{
MessageData *md = ptr;
if (md) {
g_object_unref (md->input_stream);
g_slice_free (MessageData, md);
}
}
static void
e_soup_session_message_restarted_cb (SoupMessage *message,
gpointer user_data)
{
GInputStream *input_stream;
gssize length = 0;
input_stream = e_soup_session_util_ref_message_request_body (message, &length);
g_return_if_fail (input_stream != NULL);
soup_message_set_request_body (message, NULL, input_stream, length);
g_clear_object (&input_stream);
}
#define MESSAGE_DATA_KEY "ESoupSession::message-data"
/**
* e_soup_session_util_set_message_request_body:
* @message: a #SoupMessage
* @content_type: (nullable): optional Content-Type of the @data, or %NULL
* @input_stream (transfer none): the request body data as a #GInputStream
* @length: length of the @data
*
* Sets the request body of the @message from the @input_stream of the @length, with optional
* @content_type. The function makes sure the @message request body is set again
* when the message is restarted.
*
* The @input_stream should implement the #GSeekable interface.
*
* Since: 3.46
**/
void
e_soup_session_util_set_message_request_body (SoupMessage *message,
const gchar *content_type,
GInputStream *input_stream,
gssize length)
{
MessageData *md;
g_return_if_fail (SOUP_IS_MESSAGE (message));
g_return_if_fail (G_IS_SEEKABLE (input_stream));
md = message_data_new (input_stream, length);
g_object_set_data_full (G_OBJECT (message), MESSAGE_DATA_KEY, md, message_data_free);
g_signal_connect (message, "restarted",
G_CALLBACK (e_soup_session_message_restarted_cb), NULL);
soup_message_set_request_body (message, content_type, md->input_stream, length);
}
/**
* e_soup_session_util_set_message_request_body_from_data:
* @message: a #SoupMessage
* @create_copy: whether to create copy of the @data
* @content_type: (nullable): optional Content-Type of the @data, or %NULL
* @data: the request body data
* @length: length of the @data
* @free_func: (nullable): a free function for the @data, or %NULL
*
* Sets the request body of the @message from the @data of the @length, with optional
* @content_type. The function makes sure the @message request body is set again
* when the message is restarted.
*
* When the @create_copy is %TRUE, the @free_func should be %NULL.
*
* Since: 3.46
**/
void
e_soup_session_util_set_message_request_body_from_data (SoupMessage *message,
gboolean create_copy,
const gchar *content_type,
gconstpointer data,
gssize length,
GDestroyNotify free_func)
{
GInputStream *input_stream;
g_return_if_fail (SOUP_IS_MESSAGE (message));
g_return_if_fail (data != NULL);
if (create_copy)
g_return_if_fail (free_func == NULL);
if (create_copy)
input_stream = g_memory_input_stream_new_from_data (g_memdup2 (data, length), length, g_free);
else
input_stream = g_memory_input_stream_new_from_data (data, length, free_func);
e_soup_session_util_set_message_request_body (message, content_type, input_stream, length);
g_object_unref (input_stream);
}
/**
* e_soup_session_util_ref_message_request_body:
* @message: a #SoupMessage
* @out_length: (out) (optional): length of the input stream
*
* Returns referenced request data for the @message, as being previously
* set by the e_soup_session_util_set_message_request_body() or
* e_soup_session_util_set_message_request_body_from_data().
*
* Do not call this function while the @message is queued in
* a #SoupSession, nor modify the input stream position until
* the @message lefts the #SoupSession.
*
* Returns: (nullable) (transfer full): a new #GInputStream with the request
* body being previously set, or %NULL. The @out_length is set to the length
* of the returned input stream.
*
* Since: 3.46
**/
GInputStream *
e_soup_session_util_ref_message_request_body (SoupMessage *message,
gssize *out_length)
{
MessageData *md;
g_return_val_if_fail (SOUP_IS_MESSAGE (message), NULL);
md = g_object_get_data (G_OBJECT (message), MESSAGE_DATA_KEY);
if (!md || !md->input_stream)
return NULL;
if (out_length)
*out_length = md->length;
return e_input_stream_wrapper_dup (E_INPUT_STREAM_WRAPPER (md->input_stream));
}
/**
* e_soup_session_util_get_message_bytes:
* @message: a #SoupMessage
*
* Returns bytes read from the message response, when the message send failed.
* This can be used to examine detailed error returned by the server in
* the response body.
*
* Returns: (transfer none) (nullable): read message data on failed request, or %NULL, when none had been read
*
* Since: 3.46
**/
GByteArray *
e_soup_session_util_get_message_bytes (SoupMessage *message)
{
g_return_val_if_fail (SOUP_IS_MESSAGE (message), NULL);
return g_object_get_data (G_OBJECT (message), E_SOUP_SESSION_MESSAGE_BYTES_KEY);
}
/**
* e_soup_session_util_get_force_http1_supported:
*
* Checks whether e_soup_session_set_force_http1() can be used
* to force HTTP/1 usage. This depends on the libsoup version
* the data server had been compiled with.
*
* Returns: %TRUE, when the force of HTTP/1 is supported, %FALSE otherwise
*
* Since: 3.48
**/
gboolean
e_soup_session_util_get_force_http1_supported (void)
{
#ifdef HAVE_SOUP_MESSAGE_SET_FORCE_HTTP1
return TRUE;
#else
return FALSE;
#endif
}