diff options
Diffstat (limited to 'libsoup/soup-session.c')
-rw-r--r-- | libsoup/soup-session.c | 2607 |
1 files changed, 2248 insertions, 359 deletions
diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c index 7b68d427..132554d6 100644 --- a/libsoup/soup-session.c +++ b/libsoup/soup-session.c @@ -13,13 +13,13 @@ #include "soup-session.h" #include "soup.h" -#include "soup-auth-manager-ntlm.h" +#include "soup-auth-manager.h" +#include "soup-cache-private.h" #include "soup-connection.h" -#include "soup-marshal.h" #include "soup-message-private.h" #include "soup-misc-private.h" #include "soup-message-queue.h" -#include "soup-proxy-resolver-static.h" +#include "soup-proxy-resolver-wrapper.h" #include "soup-session-private.h" #define HOST_KEEP_ALIVE 5 * 60 * 1000 /* 5 min in msecs */ @@ -31,7 +31,8 @@ * #SoupSession is the object that controls client-side HTTP. A * #SoupSession encapsulates all of the state that libsoup is keeping * on behalf of your program; cached HTTP connections, authentication - * information, etc. + * information, etc. It also keeps track of various global options + * and features that you are using. * * Most applications will only need a single #SoupSession; the primary * reason you might need multiple sessions is if you need to have @@ -42,12 +43,16 @@ * one session for the first user, and a second session for the other * user.) * - * #SoupSession itself is an abstract class, with two subclasses. If - * you are using the glib main loop, you will generally want to use - * #SoupSessionAsync, which uses non-blocking I/O and callbacks. On - * the other hand, if your application is threaded and you want to do - * synchronous I/O in a separate thread from the UI, use - * #SoupSessionSync. + * In the past, #SoupSession was an abstract class, and users needed + * to choose between #SoupSessionAsync (which always uses + * #GMainLoop<!-- -->-based I/O), or #SoupSessionSync (which always uses + * blocking I/O and can be used from multiple threads simultaneously). + * This is no longer necessary; you can (and should) use a plain + * #SoupSession, which supports both synchronous and asynchronous use. + * (When using a plain #SoupSession, soup_session_queue_message() + * behaves like it traditionally did on a #SoupSessionAsync, and + * soup_session_send_message() behaves like it traditionally did on a + * #SoupSessionSync.) **/ static void @@ -74,12 +79,16 @@ typedef struct { SoupSession *session; } SoupSessionHost; static guint soup_host_uri_hash (gconstpointer key); -gboolean soup_host_uri_equal (gconstpointer v1, gconstpointer v2); +static gboolean soup_host_uri_equal (gconstpointer v1, gconstpointer v2); typedef struct { + SoupSession *session; + gboolean disposed; + GTlsDatabase *tlsdb; char *ssl_ca_file; gboolean ssl_strict; + gboolean tlsdb_use_default; SoupMessageQueue *queue; @@ -96,37 +105,60 @@ typedef struct { guint max_conns, max_conns_per_host; guint io_timeout, idle_timeout; - /* Must hold the conn_lock before potentially creating a - * new SoupSessionHost, or adding/removing a connection. - * Must not emit signals or destroy objects while holding it. + SoupAddress *local_addr; + + /* Must hold the conn_lock before potentially creating a new + * SoupSessionHost, adding/removing a connection, + * disconnecting a connection, or moving a connection from + * IDLE to IN_USE. Must not emit signals or destroy objects + * while holding it. conn_cond is signaled when it may be + * possible for a previously-blocked message to continue. */ GMutex conn_lock; + GCond conn_cond; GMainContext *async_context; gboolean use_thread_context; + GSList *run_queue_sources; GResolver *resolver; + GProxyResolver *proxy_resolver; + gboolean proxy_use_default; + SoupURI *proxy_uri; char **http_aliases, **https_aliases; + + GHashTable *request_types; } SoupSessionPrivate; #define SOUP_SESSION_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_SESSION, SoupSessionPrivate)) +#define SOUP_IS_PLAIN_SESSION(o) (G_TYPE_FROM_INSTANCE (o) == SOUP_TYPE_SESSION) + static void free_host (SoupSessionHost *host); +static void connection_state_changed (GObject *object, GParamSpec *param, + gpointer user_data); +static void connection_disconnected (SoupConnection *conn, gpointer user_data); +static void drop_connection (SoupSession *session, SoupSessionHost *host, + SoupConnection *conn); static void auth_manager_authenticate (SoupAuthManager *manager, SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data); +static void async_run_queue (SoupSession *session); + +static void async_send_request_running (SoupSession *session, SoupMessageQueueItem *item); + #define SOUP_SESSION_MAX_CONNS_DEFAULT 10 #define SOUP_SESSION_MAX_CONNS_PER_HOST_DEFAULT 2 -#define SOUP_SESSION_MAX_REDIRECTION_COUNT 20 +#define SOUP_SESSION_MAX_RESEND_COUNT 20 #define SOUP_SESSION_USER_AGENT_BASE "libsoup/" PACKAGE_VERSION -G_DEFINE_ABSTRACT_TYPE_WITH_CODE (SoupSession, soup_session, G_TYPE_OBJECT, - soup_init (); - ) +G_DEFINE_TYPE_WITH_CODE (SoupSession, soup_session, G_TYPE_OBJECT, + soup_init (); + ) enum { REQUEST_QUEUED, @@ -144,6 +176,7 @@ enum { PROP_0, PROP_PROXY_URI, + PROP_PROXY_RESOLVER, PROP_MAX_CONNS, PROP_MAX_CONNS_PER_HOST, PROP_USE_NTLM, @@ -163,6 +196,7 @@ enum { PROP_REMOVE_FEATURE_BY_TYPE, PROP_HTTP_ALIASES, PROP_HTTPS_ALIASES, + PROP_LOCAL_ADDRESS, LAST_PROP }; @@ -173,9 +207,12 @@ soup_session_init (SoupSession *session) SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); SoupAuthManager *auth_manager; + priv->session = session; + priv->queue = soup_message_queue_new (session); g_mutex_init (&priv->conn_lock); + g_cond_init (&priv->conn_cond); priv->http_hosts = g_hash_table_new_full (soup_host_uri_hash, soup_host_uri_equal, NULL, (GDestroyNotify)free_host); @@ -189,7 +226,7 @@ soup_session_init (SoupSession *session) priv->features_cache = g_hash_table_new (NULL, NULL); - auth_manager = g_object_new (SOUP_TYPE_AUTH_MANAGER_NTLM, NULL); + auth_manager = g_object_new (SOUP_TYPE_AUTH_MANAGER, NULL); g_signal_connect (auth_manager, "authenticate", G_CALLBACK (auth_manager_authenticate), session); soup_session_feature_add_feature (SOUP_SESSION_FEATURE (auth_manager), @@ -209,6 +246,49 @@ soup_session_init (SoupSession *session) priv->http_aliases = g_new (char *, 2); priv->http_aliases[0] = (char *)g_intern_string ("*"); priv->http_aliases[1] = NULL; + + priv->request_types = g_hash_table_new (soup_str_case_hash, + soup_str_case_equal); + soup_session_add_feature_by_type (session, SOUP_TYPE_REQUEST_HTTP); + soup_session_add_feature_by_type (session, SOUP_TYPE_REQUEST_FILE); + soup_session_add_feature_by_type (session, SOUP_TYPE_REQUEST_DATA); +} + +static GObject * +soup_session_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + + object = G_OBJECT_CLASS (soup_session_parent_class)->constructor (type, n_construct_properties, construct_params); + + /* If this is a "plain" SoupSession, fix up the default + * properties values, etc. + */ + if (type == SOUP_TYPE_SESSION) { + SoupSession *session = SOUP_SESSION (object); + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + + g_clear_pointer (&priv->async_context, g_main_context_unref); + priv->async_context = g_main_context_ref_thread_default (); + priv->use_thread_context = TRUE; + + priv->io_timeout = priv->idle_timeout = 60; + + priv->http_aliases[0] = NULL; + + /* If the user overrides the proxy or tlsdb during construction, + * we don't want to needlessly resolve the extension point. So + * we just set flags saying to do it later. + */ + priv->proxy_use_default = TRUE; + priv->tlsdb_use_default = TRUE; + + soup_session_add_feature_by_type (session, SOUP_TYPE_CONTENT_DECODER); + } + + return object; } static void @@ -216,8 +296,19 @@ soup_session_dispose (GObject *object) { SoupSession *session = SOUP_SESSION (object); SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + GSList *iter; + priv->disposed = TRUE; + + for (iter = priv->run_queue_sources; iter; iter = iter->next) { + g_source_destroy (iter->data); + g_source_unref (iter->data); + } + g_clear_pointer (&priv->run_queue_sources, g_slist_free); + + priv->disposed = TRUE; soup_session_abort (session); + g_warn_if_fail (g_hash_table_size (priv->conns) == 0); while (priv->features) soup_session_remove_feature (session, priv->features->data); @@ -234,6 +325,7 @@ soup_session_finalize (GObject *object) soup_message_queue_destroy (priv->queue); g_mutex_clear (&priv->conn_lock); + g_cond_clear (&priv->conn_cond); g_hash_table_destroy (priv->http_hosts); g_hash_table_destroy (priv->https_hosts); g_hash_table_destroy (priv->conns); @@ -245,14 +337,19 @@ soup_session_finalize (GObject *object) g_free (priv->ssl_ca_file); g_clear_pointer (&priv->async_context, g_main_context_unref); + g_clear_object (&priv->local_addr); g_hash_table_destroy (priv->features_cache); g_object_unref (priv->resolver); + g_clear_object (&priv->proxy_resolver); + g_clear_pointer (&priv->proxy_uri, soup_uri_free); g_free (priv->http_aliases); g_free (priv->https_aliases); + g_hash_table_destroy (priv->request_types); + G_OBJECT_CLASS (soup_session_parent_class)->finalize (object); } @@ -342,16 +439,19 @@ set_tlsdb (SoupSession *session, GTlsDatabase *tlsdb) SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); GTlsDatabase *system_default; + priv->tlsdb_use_default = FALSE; if (tlsdb == priv->tlsdb) return; g_object_freeze_notify (G_OBJECT (session)); system_default = g_tls_backend_get_default_database (g_tls_backend_get_default ()); - if (priv->tlsdb == system_default || tlsdb == system_default) { - g_object_notify (G_OBJECT (session), "ssl-use-system-ca-file"); + if (system_default) { + if (priv->tlsdb == system_default || tlsdb == system_default) { + g_object_notify (G_OBJECT (session), "ssl-use-system-ca-file"); + } + g_object_unref (system_default); } - g_object_unref (system_default); if (priv->ssl_ca_file) { g_free (priv->ssl_ca_file); @@ -375,6 +475,8 @@ set_use_system_ca_file (SoupSession *session, gboolean use_system_ca_file) SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); GTlsDatabase *system_default; + priv->tlsdb_use_default = FALSE; + system_default = g_tls_backend_get_default_database (g_tls_backend_get_default ()); if (use_system_ca_file) @@ -382,7 +484,7 @@ set_use_system_ca_file (SoupSession *session, gboolean use_system_ca_file) else if (priv->tlsdb == system_default) set_tlsdb (session, NULL); - g_object_unref (system_default); + g_clear_object (&system_default); } static void @@ -392,6 +494,7 @@ set_ssl_ca_file (SoupSession *session, const char *ssl_ca_file) GTlsDatabase *tlsdb; GError *error = NULL; + priv->tlsdb_use_default = FALSE; if (!g_strcmp0 (priv->ssl_ca_file, ssl_ca_file)) return; @@ -406,6 +509,7 @@ set_ssl_ca_file (SoupSession *session, const char *ssl_ca_file) path = g_build_filename (cwd, ssl_ca_file, NULL); tlsdb = g_tls_file_database_new (path, &error); g_free (path); + g_free (cwd); } if (error) { @@ -419,10 +523,15 @@ set_ssl_ca_file (SoupSession *session, const char *ssl_ca_file) } set_tlsdb (session, tlsdb); - g_object_unref (tlsdb); + if (tlsdb) { + g_object_unref (tlsdb); - priv->ssl_ca_file = g_strdup (ssl_ca_file); - g_object_notify (G_OBJECT (session), "ssl-ca-file"); + priv->ssl_ca_file = g_strdup (ssl_ca_file); + g_object_notify (G_OBJECT (session), "ssl-ca-file"); + } else if (priv->ssl_ca_file) { + g_clear_pointer (&priv->ssl_ca_file, g_free); + g_object_notify (G_OBJECT (session), "ssl-ca-file"); + } g_object_thaw_notify (G_OBJECT (session)); } @@ -451,29 +560,60 @@ set_aliases (char ***variable, char **value) } static void +set_proxy_resolver (SoupSession *session, SoupURI *uri, + SoupProxyURIResolver *soup_resolver, + GProxyResolver *g_resolver) +{ + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + soup_session_remove_feature_by_type (session, SOUP_TYPE_PROXY_URI_RESOLVER); + G_GNUC_END_IGNORE_DEPRECATIONS; + g_clear_object (&priv->proxy_resolver); + g_clear_pointer (&priv->proxy_uri, soup_uri_free); + priv->proxy_use_default = FALSE; + + if (uri) { + char *uri_string; + + priv->proxy_uri = soup_uri_copy (uri); + uri_string = soup_uri_to_string_internal (uri, FALSE, TRUE); + priv->proxy_resolver = g_simple_proxy_resolver_new (uri_string, NULL); + g_free (uri_string); + } else if (soup_resolver) { + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + if (SOUP_IS_PROXY_RESOLVER_DEFAULT (soup_resolver)) + priv->proxy_resolver = g_object_ref (g_proxy_resolver_get_default ()); + else + priv->proxy_resolver = soup_proxy_resolver_wrapper_new (soup_resolver); + G_GNUC_END_IGNORE_DEPRECATIONS; + } else if (g_resolver) + priv->proxy_resolver = g_object_ref (g_resolver); +} + +static void soup_session_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { SoupSession *session = SOUP_SESSION (object); SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); - SoupURI *uri; const char *user_agent; SoupSessionFeature *feature; + GMainContext *async_context; switch (prop_id) { + case PROP_LOCAL_ADDRESS: + priv->local_addr = g_value_dup_object (value); + break; case PROP_PROXY_URI: - uri = g_value_get_boxed (value); - - if (uri) { - soup_session_remove_feature_by_type (session, SOUP_TYPE_PROXY_RESOLVER); - feature = SOUP_SESSION_FEATURE (soup_proxy_resolver_static_new (uri)); - soup_session_add_feature (session, feature); - g_object_unref (feature); - } else - soup_session_remove_feature_by_type (session, SOUP_TYPE_PROXY_RESOLVER_STATIC); - + set_proxy_resolver (session, g_value_get_boxed (value), + NULL, NULL); soup_session_abort (session); break; + case PROP_PROXY_RESOLVER: + set_proxy_resolver (session, NULL, NULL, + g_value_get_object (value)); + break; case PROP_MAX_CONNS: priv->max_conns = g_value_get_int (value); break; @@ -481,7 +621,8 @@ soup_session_set_property (GObject *object, guint prop_id, priv->max_conns_per_host = g_value_get_int (value); break; case PROP_USE_NTLM: - feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER_NTLM); + g_return_if_fail (!SOUP_IS_PLAIN_SESSION (session)); + feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER); if (feature) { if (g_value_get_boolean (value)) soup_session_feature_add_feature (feature, SOUP_TYPE_AUTH_NTLM); @@ -503,11 +644,16 @@ soup_session_set_property (GObject *object, guint prop_id, priv->ssl_strict = g_value_get_boolean (value); break; case PROP_ASYNC_CONTEXT: - priv->async_context = g_value_get_pointer (value); + async_context = g_value_get_pointer (value); + if (async_context && async_context != g_main_context_get_thread_default ()) + g_return_if_fail (!SOUP_IS_PLAIN_SESSION (session)); + priv->async_context = async_context; if (priv->async_context) g_main_context_ref (priv->async_context); break; case PROP_USE_THREAD_CONTEXT: + if (!g_value_get_boolean (value)) + g_return_if_fail (!SOUP_IS_PLAIN_SESSION (session)); priv->use_thread_context = g_value_get_boolean (value); if (priv->use_thread_context) { if (priv->async_context) @@ -575,6 +721,30 @@ soup_session_set_property (GObject *object, guint prop_id, } } +static GProxyResolver * +get_proxy_resolver (SoupSession *session) +{ + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + + if (priv->proxy_use_default) { + priv->proxy_resolver = g_object_ref (g_proxy_resolver_get_default ()); + priv->proxy_use_default = FALSE; + } + return priv->proxy_resolver; +} + +static GTlsDatabase * +get_tls_database (SoupSession *session) +{ + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + + if (priv->tlsdb_use_default) { + priv->tlsdb = g_tls_backend_get_default_database (g_tls_backend_get_default ()); + priv->tlsdb_use_default = FALSE; + } + return priv->tlsdb; +} + static void soup_session_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) @@ -585,14 +755,14 @@ soup_session_get_property (GObject *object, guint prop_id, GTlsDatabase *tlsdb; switch (prop_id) { + case PROP_LOCAL_ADDRESS: + g_value_set_object (value, priv->local_addr); + break; case PROP_PROXY_URI: - feature = soup_session_get_feature (session, SOUP_TYPE_PROXY_RESOLVER_STATIC); - if (feature) { - g_object_get_property (G_OBJECT (feature), - SOUP_PROXY_RESOLVER_STATIC_PROXY_URI, - value); - } else - g_value_set_boxed (value, NULL); + g_value_set_boxed (value, priv->proxy_uri); + break; + case PROP_PROXY_RESOLVER: + g_value_set_object (value, get_proxy_resolver (session)); break; case PROP_MAX_CONNS: g_value_set_int (value, priv->max_conns); @@ -601,7 +771,7 @@ soup_session_get_property (GObject *object, guint prop_id, g_value_set_int (value, priv->max_conns_per_host); break; case PROP_USE_NTLM: - feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER_NTLM); + feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER); if (feature) g_value_set_boolean (value, soup_session_feature_has_feature (feature, SOUP_TYPE_AUTH_NTLM)); else @@ -612,11 +782,11 @@ soup_session_get_property (GObject *object, guint prop_id, break; case PROP_SSL_USE_SYSTEM_CA_FILE: tlsdb = g_tls_backend_get_default_database (g_tls_backend_get_default ()); - g_value_set_boolean (value, priv->tlsdb == tlsdb); - g_object_unref (tlsdb); + g_value_set_boolean (value, get_tls_database (session) == tlsdb); + g_clear_object (&tlsdb); break; case PROP_TLS_DATABASE: - g_value_set_object (value, priv->tlsdb); + g_value_set_object (value, get_tls_database (session)); break; case PROP_SSL_STRICT: g_value_set_boolean (value, priv->ssl_strict); @@ -654,59 +824,57 @@ soup_session_get_property (GObject *object, guint prop_id, } } -static gboolean -uri_is_http (SoupSessionPrivate *priv, SoupURI *uri) +/** + * soup_session_new: + * + * Creates a #SoupSession with the default options. + * + * Return value: the new session. + * + * Since: 2.42 + */ +SoupSession * +soup_session_new (void) { - int i; - - if (uri->scheme == SOUP_URI_SCHEME_HTTP) - return TRUE; - else if (uri->scheme == SOUP_URI_SCHEME_HTTPS) - return FALSE; - else if (!priv->http_aliases) - return FALSE; - - for (i = 0; priv->http_aliases[i]; i++) { - if (uri->scheme == priv->http_aliases[i]) - return TRUE; - } - - if (!priv->http_aliases[1] && !strcmp (priv->http_aliases[0], "*")) - return TRUE; - else - return FALSE; + return g_object_new (SOUP_TYPE_SESSION, NULL); } -static gboolean -uri_is_https (SoupSessionPrivate *priv, SoupURI *uri) +/** + * soup_session_new_with_options: + * @optname1: name of first property to set + * @...: value of @optname1, followed by additional property/value pairs + * + * Creates a #SoupSession with the specified options. + * + * Return value: the new session. + * + * Since: 2.42 + */ +SoupSession * +soup_session_new_with_options (const char *optname1, + ...) { - int i; - - if (uri->scheme == SOUP_URI_SCHEME_HTTPS) - return TRUE; - else if (uri->scheme == SOUP_URI_SCHEME_HTTP) - return FALSE; - else if (!priv->https_aliases) - return FALSE; + SoupSession *session; + va_list ap; - for (i = 0; priv->https_aliases[i]; i++) { - if (uri->scheme == priv->https_aliases[i]) - return TRUE; - } + va_start (ap, optname1); + session = (SoupSession *)g_object_new_valist (SOUP_TYPE_SESSION, + optname1, ap); + va_end (ap); - return FALSE; + return session; } /** * soup_session_get_async_context: * @session: a #SoupSession * - * Gets @session's async_context. This does not add a ref to the - * context, so you will need to ref it yourself if you want it to - * outlive its session. + * Gets @session's #SoupSession:async-context. This does not add a ref + * to the context, so you will need to ref it yourself if you want it + * to outlive its session. * - * If #SoupSession:use-thread-context is true, this will return the - * current thread-default main context. + * For a modern #SoupSession, this will always just return the + * thread-default #GMainContext, and so is not especially useful. * * Return value: (transfer none): @session's #GMainContext, which may * be %NULL @@ -727,6 +895,10 @@ soup_session_get_async_context (SoupSession *session) /* Hosts */ +/* Note that we can't use soup_uri_host_hash() and soup_uri_host_equal() + * because we want to ignore the protocol; http://example.com and + * webcal://example.com are the same host. + */ static guint soup_host_uri_hash (gconstpointer key) { @@ -737,7 +909,7 @@ soup_host_uri_hash (gconstpointer key) return uri->port + soup_str_case_hash (uri->host); } -gboolean +static gboolean soup_host_uri_equal (gconstpointer v1, gconstpointer v2) { const SoupURI *one = v1; @@ -764,7 +936,7 @@ soup_session_host_new (SoupSession *session, SoupURI *uri) host->uri->scheme != SOUP_URI_SCHEME_HTTPS) { SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); - if (uri_is_https (priv, host->uri)) + if (soup_uri_is_https (host->uri, priv->https_aliases)) host->uri->scheme = SOUP_URI_SCHEME_HTTPS; else host->uri->scheme = SOUP_URI_SCHEME_HTTP; @@ -788,7 +960,7 @@ get_host_for_uri (SoupSession *session, SoupURI *uri) SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); SoupSessionHost *host; - if (uri_is_https (priv, uri)) + if (soup_uri_is_https (uri, priv->https_aliases)) host = g_hash_table_lookup (priv->https_hosts, uri); else host = g_hash_table_lookup (priv->http_hosts, uri); @@ -797,7 +969,7 @@ get_host_for_uri (SoupSession *session, SoupURI *uri) host = soup_session_host_new (session, uri); - if (uri_is_https (priv, uri)) + if (soup_uri_is_https (uri, priv->https_aliases)) g_hash_table_insert (priv->https_hosts, host->uri, host); else g_hash_table_insert (priv->http_hosts, host->uri, host); @@ -805,10 +977,7 @@ get_host_for_uri (SoupSession *session, SoupURI *uri) return host; } -/* Note: get_host_for_message doesn't lock the conn_lock. The caller - * must do it itself if there's a chance the host doesn't already - * exist. - */ +/* Requires conn_lock to be locked */ static SoupSessionHost * get_host_for_message (SoupSession *session, SoupMessage *msg) { @@ -818,8 +987,7 @@ get_host_for_message (SoupSession *session, SoupMessage *msg) static void free_host (SoupSessionHost *host) { - g_slist_free_full (host->connections, - (GDestroyNotify) soup_connection_disconnect); + g_warn_if_fail (host->connections == NULL); if (host->keep_alive_src) { g_source_destroy (host->keep_alive_src); @@ -832,19 +1000,11 @@ free_host (SoupSessionHost *host) } static void -soup_session_real_auth_required (SoupSession *session, SoupMessage *msg, - SoupAuth *auth, gboolean retrying) -{ - g_signal_emit (session, signals[AUTHENTICATE], 0, msg, auth, retrying); -} - -static void auth_manager_authenticate (SoupAuthManager *manager, SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer session) { - SOUP_SESSION_GET_CLASS (session)->auth_required ( - session, msg, auth, retrying); + g_signal_emit (session, signals[AUTHENTICATE], 0, msg, auth, retrying); } #define SOUP_SESSION_WOULD_REDIRECT_AS_GET(session, msg) \ @@ -911,7 +1071,8 @@ soup_session_would_redirect (SoupSession *session, SoupMessage *msg) if (!new_uri) return FALSE; if (!new_uri->host || !*new_uri->host || - (!uri_is_http (priv, new_uri) && !uri_is_https (priv, new_uri))) { + (!soup_uri_is_http (new_uri, priv->http_aliases) && + !soup_uri_is_https (new_uri, priv->https_aliases))) { soup_uri_free (new_uri); return FALSE; } @@ -947,27 +1108,12 @@ soup_session_would_redirect (SoupSession *session, SoupMessage *msg) gboolean soup_session_redirect_message (SoupSession *session, SoupMessage *msg) { - SoupMessageQueueItem *item; SoupURI *new_uri; new_uri = redirection_uri (msg); if (!new_uri) return FALSE; - item = soup_message_queue_lookup (soup_session_get_queue (session), msg); - if (!item) { - soup_uri_free (new_uri); - return FALSE; - } - if (item->redirection_count >= SOUP_SESSION_MAX_REDIRECTION_COUNT) { - soup_uri_free (new_uri); - soup_session_cancel_message (session, msg, SOUP_STATUS_TOO_MANY_REDIRECTS); - soup_message_queue_item_unref (item); - return FALSE; - } - item->redirection_count++; - soup_message_queue_item_unref (item); - if (SOUP_SESSION_WOULD_REDIRECT_AS_GET (session, msg)) { if (msg->method != SOUP_METHOD_HEAD) { g_object_set (msg, @@ -1010,15 +1156,67 @@ redirect_handler (SoupMessage *msg, gpointer user_data) soup_session_redirect_message (session, msg); } +static void +re_emit_connection_event (SoupConnection *conn, + GSocketClientEvent event, + GIOStream *connection, + gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + + soup_message_network_event (item->msg, event, connection); +} + +static void +soup_session_set_item_connection (SoupSession *session, + SoupMessageQueueItem *item, + SoupConnection *conn) +{ + if (item->conn) { + g_signal_handlers_disconnect_by_func (item->conn, re_emit_connection_event, item); + g_object_unref (item->conn); + } + + item->conn = conn; + soup_message_set_connection (item->msg, conn); + + if (item->conn) { + g_object_ref (item->conn); + g_signal_connect (item->conn, "event", + G_CALLBACK (re_emit_connection_event), item); + } +} + +static void +message_restarted (SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + + if (item->conn && + (!soup_message_is_keepalive (msg) || + SOUP_STATUS_IS_REDIRECTION (msg->status_code))) { + if (soup_connection_get_state (item->conn) == SOUP_CONNECTION_IN_USE) + soup_connection_set_state (item->conn, SOUP_CONNECTION_IDLE); + soup_session_set_item_connection (item->session, item, NULL); + } + + soup_message_cleanup_response (msg); +} + SoupMessageQueueItem * soup_session_append_queue_item (SoupSession *session, SoupMessage *msg, + gboolean async, gboolean new_api, SoupSessionCallback callback, gpointer user_data) { SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); SoupMessageQueueItem *item; SoupSessionHost *host; + soup_message_cleanup_response (msg); + item = soup_message_queue_append (priv->queue, msg, callback, user_data); + item->async = async; + item->new_api = new_api; g_mutex_lock (&priv->conn_lock); host = get_host_for_message (session, item->msg); @@ -1030,6 +1228,8 @@ soup_session_append_queue_item (SoupSession *session, SoupMessage *msg, msg, "got_body", "Location", G_CALLBACK (redirect_handler), item); } + g_signal_connect (msg, "restarted", + G_CALLBACK (message_restarted), item); g_signal_emit (session, signals[REQUEST_QUEUED], 0, msg); @@ -1037,7 +1237,7 @@ soup_session_append_queue_item (SoupSession *session, SoupMessage *msg, return item; } -void +static void soup_session_send_queue_item (SoupSession *session, SoupMessageQueueItem *item, SoupMessageCompletionFn completion_cb) @@ -1076,9 +1276,9 @@ soup_session_send_queue_item (SoupSession *session, soup_connection_send_request (item->conn, item, completion_cb, item); } -gboolean +static gboolean soup_session_cleanup_connections (SoupSession *session, - gboolean prune_idle) + gboolean cleanup_idle) { SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); GSList *conns = NULL, *c; @@ -1091,8 +1291,11 @@ soup_session_cleanup_connections (SoupSession *session, while (g_hash_table_iter_next (&iter, &conn, &host)) { state = soup_connection_get_state (conn); if (state == SOUP_CONNECTION_REMOTE_DISCONNECTED || - (prune_idle && state == SOUP_CONNECTION_IDLE)) + (cleanup_idle && state == SOUP_CONNECTION_IDLE)) { conns = g_slist_prepend (conns, g_object_ref (conn)); + g_hash_table_iter_remove (&iter); + drop_connection (session, host, conn); + } } g_mutex_unlock (&priv->conn_lock); @@ -1116,6 +1319,15 @@ free_unused_host (gpointer user_data) SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (host->session); g_mutex_lock (&priv->conn_lock); + + /* In a multithreaded session, a connection might have been + * added while we were waiting for conn_lock. + */ + if (host->connections) { + g_mutex_unlock (&priv->conn_lock); + return FALSE; + } + /* This will free the host in addition to removing it from the * hash table */ @@ -1129,17 +1341,15 @@ free_unused_host (gpointer user_data) } static void -connection_disconnected (SoupConnection *conn, gpointer user_data) +drop_connection (SoupSession *session, SoupSessionHost *host, SoupConnection *conn) { - SoupSession *session = user_data; SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); - SoupSessionHost *host; - g_mutex_lock (&priv->conn_lock); + /* Note: caller must hold conn_lock, and must remove @conn + * from priv->conns itself. + */ - host = g_hash_table_lookup (priv->conns, conn); if (host) { - g_hash_table_remove (priv->conns, conn); host->connections = g_slist_remove (host->connections, conn); host->num_conns--; @@ -1161,106 +1371,430 @@ connection_disconnected (SoupConnection *conn, gpointer user_data) } g_signal_handlers_disconnect_by_func (conn, connection_disconnected, session); + g_signal_handlers_disconnect_by_func (conn, connection_state_changed, session); priv->num_conns--; - g_mutex_unlock (&priv->conn_lock); g_object_unref (conn); } -SoupMessageQueueItem * -soup_session_make_connect_message (SoupSession *session, - SoupConnection *conn) +static void +connection_disconnected (SoupConnection *conn, gpointer user_data) +{ + SoupSession *session = user_data; + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + SoupSessionHost *host; + + g_mutex_lock (&priv->conn_lock); + + host = g_hash_table_lookup (priv->conns, conn); + if (host) + g_hash_table_remove (priv->conns, conn); + drop_connection (session, host, conn); + + g_mutex_unlock (&priv->conn_lock); + + soup_session_kick_queue (session); +} + +static void +connection_state_changed (GObject *object, GParamSpec *param, gpointer user_data) +{ + SoupSession *session = user_data; + SoupConnection *conn = SOUP_CONNECTION (object); + + if (soup_connection_get_state (conn) == SOUP_CONNECTION_IDLE) + soup_session_kick_queue (session); +} + +SoupMessageQueue * +soup_session_get_queue (SoupSession *session) { + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + + return priv->queue; +} + +static void +soup_session_unqueue_item (SoupSession *session, + SoupMessageQueueItem *item) +{ + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + SoupSessionHost *host; + + if (item->conn) { + if (item->msg->method != SOUP_METHOD_CONNECT || + !SOUP_STATUS_IS_SUCCESSFUL (item->msg->status_code)) + soup_connection_set_state (item->conn, SOUP_CONNECTION_IDLE); + soup_session_set_item_connection (session, item, NULL); + } + + if (item->state != SOUP_MESSAGE_FINISHED) { + g_warning ("finished an item with state %d", item->state); + return; + } + + soup_message_queue_remove (priv->queue, item); + + g_mutex_lock (&priv->conn_lock); + host = get_host_for_message (session, item->msg); + host->num_messages--; + g_cond_broadcast (&priv->conn_cond); + g_mutex_unlock (&priv->conn_lock); + + /* g_signal_handlers_disconnect_by_func doesn't work if you + * have a metamarshal, meaning it doesn't work with + * soup_message_add_header_handler() + */ + g_signal_handlers_disconnect_matched (item->msg, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, item); + g_signal_emit (session, signals[REQUEST_UNQUEUED], 0, item->msg); + soup_message_queue_item_unref (item); +} + +static void +soup_session_set_item_status (SoupSession *session, + SoupMessageQueueItem *item, + guint status_code, + GError *error) +{ + SoupURI *uri = NULL; + + switch (status_code) { + case SOUP_STATUS_CANT_RESOLVE: + case SOUP_STATUS_CANT_CONNECT: + uri = soup_message_get_uri (item->msg); + break; + + case SOUP_STATUS_CANT_RESOLVE_PROXY: + case SOUP_STATUS_CANT_CONNECT_PROXY: + if (item->conn) + uri = soup_connection_get_proxy_uri (item->conn); + break; + + case SOUP_STATUS_SSL_FAILED: + if (!g_tls_backend_supports_tls (g_tls_backend_get_default ())) { + soup_message_set_status_full (item->msg, status_code, + "TLS/SSL support not available; install glib-networking"); + return; + } + break; + + default: + break; + } + + if (error) + soup_message_set_status_full (item->msg, status_code, error->message); + else if (uri && uri->host) { + char *msg = g_strdup_printf ("%s (%s)", + soup_status_get_phrase (status_code), + uri->host); + soup_message_set_status_full (item->msg, status_code, msg); + g_free (msg); + } else + soup_message_set_status (item->msg, status_code); +} + + +static void +message_completed (SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + + if (item->async) + soup_session_kick_queue (item->session); + + if (item->state != SOUP_MESSAGE_RESTARTING) { + item->state = SOUP_MESSAGE_FINISHING; + + if (item->new_api && !item->async) + soup_session_process_queue_item (item->session, item, NULL, TRUE); + } +} + +static guint +status_from_connect_error (SoupMessageQueueItem *item, GError *error) +{ + guint status; + + if (!error) + return SOUP_STATUS_OK; + + if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_NOT_TLS)) { + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (item->session); + SoupSessionHost *host; + + g_mutex_lock (&priv->conn_lock); + host = get_host_for_message (item->session, item->msg); + if (!host->ssl_fallback) { + host->ssl_fallback = TRUE; + status = SOUP_STATUS_TRY_AGAIN; + } else + status = SOUP_STATUS_SSL_FAILED; + g_mutex_unlock (&priv->conn_lock); + } else if (error->domain == G_TLS_ERROR) + status = SOUP_STATUS_SSL_FAILED; + else if (error->domain == G_RESOLVER_ERROR) + status = SOUP_STATUS_CANT_RESOLVE; + else if (error->domain == G_IO_ERROR) { + if (error->code == G_IO_ERROR_CANCELLED) + status = SOUP_STATUS_CANCELLED; + else if (error->code == G_IO_ERROR_HOST_UNREACHABLE || + error->code == G_IO_ERROR_NETWORK_UNREACHABLE || + error->code == G_IO_ERROR_CONNECTION_REFUSED) + status = SOUP_STATUS_CANT_CONNECT; + else if (error->code == G_IO_ERROR_PROXY_FAILED || + error->code == G_IO_ERROR_PROXY_AUTH_FAILED || + error->code == G_IO_ERROR_PROXY_NEED_AUTH || + error->code == G_IO_ERROR_PROXY_NOT_ALLOWED) + status = SOUP_STATUS_CANT_CONNECT_PROXY; + else + status = SOUP_STATUS_IO_ERROR; + } else + status = SOUP_STATUS_IO_ERROR; + + if (item->conn && soup_connection_is_via_proxy (item->conn)) + return soup_status_proxify (status); + else + return status; +} + +static void +tunnel_complete (SoupMessageQueueItem *tunnel_item, + guint status, GError *error) +{ + SoupMessageQueueItem *item = tunnel_item->related; + SoupSession *session = tunnel_item->session; + + soup_message_finished (tunnel_item->msg); + soup_message_queue_item_unref (tunnel_item); + + if (item->msg->status_code) + item->state = SOUP_MESSAGE_FINISHING; + soup_message_set_https_status (item->msg, item->conn); + + item->error = error; + if (!status) + status = status_from_connect_error (item, error); + if (!SOUP_STATUS_IS_SUCCESSFUL (status)) { + soup_connection_disconnect (item->conn); + soup_session_set_item_connection (session, item, NULL); + if (!item->new_api || item->msg->status_code == 0) + soup_session_set_item_status (session, item, status, error); + } + + item->state = SOUP_MESSAGE_READY; + if (item->async) + soup_session_kick_queue (session); + soup_message_queue_item_unref (item); +} + +static void +tunnel_handshake_complete (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + SoupConnection *conn = SOUP_CONNECTION (object); + SoupMessageQueueItem *tunnel_item = user_data; + GError *error = NULL; + + soup_connection_start_ssl_finish (conn, result, &error); + tunnel_complete (tunnel_item, 0, error); +} + +static void +tunnel_message_completed (SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *tunnel_item = user_data; + SoupMessageQueueItem *item = tunnel_item->related; + SoupSession *session = tunnel_item->session; + guint status; + + if (tunnel_item->state == SOUP_MESSAGE_RESTARTING) { + soup_message_restarted (msg); + if (tunnel_item->conn) { + tunnel_item->state = SOUP_MESSAGE_RUNNING; + soup_session_send_queue_item (session, tunnel_item, + tunnel_message_completed); + return; + } + + soup_message_set_status (msg, SOUP_STATUS_TRY_AGAIN); + } + + tunnel_item->state = SOUP_MESSAGE_FINISHED; + soup_session_unqueue_item (session, tunnel_item); + + status = tunnel_item->msg->status_code; + if (!SOUP_STATUS_IS_SUCCESSFUL (status)) { + tunnel_complete (tunnel_item, status, NULL); + return; + } + + if (tunnel_item->async) { + soup_connection_start_ssl_async (item->conn, item->cancellable, + tunnel_handshake_complete, + tunnel_item); + } else { + GError *error = NULL; + + soup_connection_start_ssl_sync (item->conn, item->cancellable, &error); + tunnel_complete (tunnel_item, 0, error); + } +} + +static void +tunnel_connect (SoupMessageQueueItem *item) +{ + SoupSession *session = item->session; + SoupMessageQueueItem *tunnel_item; SoupURI *uri; SoupMessage *msg; - SoupMessageQueueItem *item; - uri = soup_connection_get_remote_uri (conn); + item->state = SOUP_MESSAGE_TUNNELING; + + uri = soup_connection_get_remote_uri (item->conn); msg = soup_message_new_from_uri (SOUP_METHOD_CONNECT, uri); soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT); - item = soup_session_append_queue_item (session, msg, NULL, NULL); - soup_message_queue_item_set_connection (item, conn); + tunnel_item = soup_session_append_queue_item (session, msg, + item->async, FALSE, + NULL, NULL); g_object_unref (msg); - item->state = SOUP_MESSAGE_RUNNING; + tunnel_item->related = item; + soup_message_queue_item_ref (item); + soup_session_set_item_connection (session, tunnel_item, item->conn); + tunnel_item->state = SOUP_MESSAGE_RUNNING; - g_signal_emit (session, signals[TUNNELING], 0, conn); - return item; + g_signal_emit (session, signals[TUNNELING], 0, tunnel_item->conn); + + soup_session_send_queue_item (session, tunnel_item, + tunnel_message_completed); } -gboolean -soup_session_get_connection (SoupSession *session, - SoupMessageQueueItem *item, - gboolean *try_pruning) +static void +connect_complete (SoupMessageQueueItem *item, SoupConnection *conn, GError *error) +{ + SoupSession *session = item->session; + guint status; + + soup_message_set_https_status (item->msg, item->conn); + + if (!error) { + item->state = SOUP_MESSAGE_CONNECTED; + return; + } + + item->error = error; + status = status_from_connect_error (item, error); + soup_connection_disconnect (conn); + if (item->state == SOUP_MESSAGE_CONNECTING) { + if (!item->new_api || item->msg->status_code == 0) + soup_session_set_item_status (session, item, status, error); + soup_session_set_item_connection (session, item, NULL); + item->state = SOUP_MESSAGE_READY; + } +} + +static void +connect_async_complete (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + SoupConnection *conn = SOUP_CONNECTION (object); + SoupMessageQueueItem *item = user_data; + GError *error = NULL; + + soup_connection_connect_finish (conn, result, &error); + connect_complete (item, conn, error); + + if (item->state == SOUP_MESSAGE_CONNECTED || + item->state == SOUP_MESSAGE_READY) + async_run_queue (item->session); + else + soup_session_kick_queue (item->session); + + soup_message_queue_item_unref (item); +} + +/* requires conn_lock */ +static SoupConnection * +get_connection_for_host (SoupSession *session, + SoupMessageQueueItem *item, + SoupSessionHost *host, + gboolean need_new_connection, + gboolean *try_cleanup) { SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); SoupConnection *conn; - SoupSessionHost *host; GSList *conns; int num_pending = 0; - gboolean need_new_connection; + GProxyResolver *proxy_resolver; + GTlsDatabase *tlsdb; + + if (priv->disposed) + return FALSE; if (item->conn) { g_return_val_if_fail (soup_connection_get_state (item->conn) != SOUP_CONNECTION_DISCONNECTED, FALSE); - return TRUE; + return item->conn; } - need_new_connection = - (soup_message_get_flags (item->msg) & SOUP_MESSAGE_NEW_CONNECTION) || - !SOUP_METHOD_IS_IDEMPOTENT (item->msg->method); - - g_mutex_lock (&priv->conn_lock); - - host = get_host_for_message (session, item->msg); for (conns = host->connections; conns; conns = conns->next) { - if (!need_new_connection && soup_connection_get_state (conns->data) == SOUP_CONNECTION_IDLE) { - soup_connection_set_state (conns->data, SOUP_CONNECTION_IN_USE); - g_mutex_unlock (&priv->conn_lock); - soup_message_queue_item_set_connection (item, conns->data); - soup_message_set_https_status (item->msg, item->conn); - return TRUE; - } else if (soup_connection_get_state (conns->data) == SOUP_CONNECTION_CONNECTING) + conn = conns->data; + + if (!need_new_connection && soup_connection_get_state (conn) == SOUP_CONNECTION_IDLE) { + soup_connection_set_state (conn, SOUP_CONNECTION_IN_USE); + return conn; + } else if (soup_connection_get_state (conn) == SOUP_CONNECTION_CONNECTING) num_pending++; } /* Limit the number of pending connections; num_messages / 2 * is somewhat arbitrary... */ - if (num_pending > host->num_messages / 2) { - g_mutex_unlock (&priv->conn_lock); - return FALSE; - } + if (num_pending > host->num_messages / 2) + return NULL; if (host->num_conns >= priv->max_conns_per_host) { if (need_new_connection) - *try_pruning = TRUE; - g_mutex_unlock (&priv->conn_lock); - return FALSE; + *try_cleanup = TRUE; + return NULL; } if (priv->num_conns >= priv->max_conns) { - *try_pruning = TRUE; - g_mutex_unlock (&priv->conn_lock); - return FALSE; + *try_cleanup = TRUE; + return NULL; } + proxy_resolver = get_proxy_resolver (session); + tlsdb = get_tls_database (session); + conn = g_object_new ( SOUP_TYPE_CONNECTION, SOUP_CONNECTION_REMOTE_URI, host->uri, - SOUP_CONNECTION_PROXY_RESOLVER, soup_session_get_feature (session, SOUP_TYPE_PROXY_URI_RESOLVER), - SOUP_CONNECTION_SSL, uri_is_https (priv, soup_message_get_uri (item->msg)), - SOUP_CONNECTION_SSL_CREDENTIALS, priv->tlsdb, - SOUP_CONNECTION_SSL_STRICT, (priv->tlsdb != NULL) && priv->ssl_strict, + SOUP_CONNECTION_PROXY_RESOLVER, proxy_resolver, + SOUP_CONNECTION_SSL, soup_uri_is_https (soup_message_get_uri (item->msg), priv->https_aliases), + SOUP_CONNECTION_SSL_CREDENTIALS, tlsdb, + SOUP_CONNECTION_SSL_STRICT, priv->ssl_strict && (tlsdb != NULL || SOUP_IS_PLAIN_SESSION (session)), SOUP_CONNECTION_ASYNC_CONTEXT, priv->async_context, SOUP_CONNECTION_USE_THREAD_CONTEXT, priv->use_thread_context, SOUP_CONNECTION_TIMEOUT, priv->io_timeout, SOUP_CONNECTION_IDLE_TIMEOUT, priv->idle_timeout, SOUP_CONNECTION_SSL_FALLBACK, host->ssl_fallback, + SOUP_CONNECTION_LOCAL_ADDRESS, priv->local_addr, NULL); g_signal_connect (conn, "disconnected", G_CALLBACK (connection_disconnected), session); + g_signal_connect (conn, "notify::state", + G_CALLBACK (connection_state_changed), + session); + /* This is a debugging-related signal, and so can ignore the + * usual rule about not emitting signals while holding + * conn_lock. + */ g_signal_emit (session, signals[CONNECTION_CREATED], 0, conn); g_hash_table_insert (priv->conns, conn, host); @@ -1275,98 +1809,227 @@ soup_session_get_connection (SoupSession *session, host->keep_alive_src = NULL; } - g_mutex_unlock (&priv->conn_lock); - soup_message_queue_item_set_connection (item, conn); - return TRUE; + return conn; } -SoupMessageQueue * -soup_session_get_queue (SoupSession *session) +static gboolean +get_connection (SoupMessageQueueItem *item, gboolean *should_cleanup) { + SoupSession *session = item->session; SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + SoupSessionHost *host; + SoupConnection *conn = NULL; + gboolean my_should_cleanup = FALSE; + gboolean need_new_connection; - return priv->queue; -} + soup_session_cleanup_connections (session, FALSE); -void -soup_session_unqueue_item (SoupSession *session, - SoupMessageQueueItem *item) -{ - SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); - SoupSessionHost *host; + need_new_connection = + (soup_message_get_flags (item->msg) & SOUP_MESSAGE_NEW_CONNECTION) || + (!(soup_message_get_flags (item->msg) & SOUP_MESSAGE_IDEMPOTENT) && + !SOUP_METHOD_IS_IDEMPOTENT (item->msg->method)); - if (item->conn) { - if (soup_connection_get_state (item->conn) == SOUP_CONNECTION_IN_USE) - soup_connection_set_state (item->conn, SOUP_CONNECTION_IDLE); - soup_message_queue_item_set_connection (item, NULL); + g_mutex_lock (&priv->conn_lock); + host = get_host_for_message (session, item->msg); + while (TRUE) { + conn = get_connection_for_host (session, item, host, + need_new_connection, + &my_should_cleanup); + if (conn || item->async) + break; + + if (my_should_cleanup) { + g_mutex_unlock (&priv->conn_lock); + soup_session_cleanup_connections (session, TRUE); + g_mutex_lock (&priv->conn_lock); + + my_should_cleanup = FALSE; + continue; + } + + g_cond_wait (&priv->conn_cond, &priv->conn_lock); } + g_mutex_unlock (&priv->conn_lock); - if (item->state != SOUP_MESSAGE_FINISHED) { - g_warning ("finished an item with state %d", item->state); - return; + if (!conn) { + if (should_cleanup) + *should_cleanup = my_should_cleanup; + return FALSE; } - soup_message_queue_remove (priv->queue, item); + soup_session_set_item_connection (session, item, conn); - g_mutex_lock (&priv->conn_lock); - host = get_host_for_message (session, item->msg); - host->num_messages--; - g_mutex_unlock (&priv->conn_lock); + if (soup_connection_get_state (item->conn) != SOUP_CONNECTION_NEW) { + item->state = SOUP_MESSAGE_READY; + soup_message_set_https_status (item->msg, item->conn); + return TRUE; + } - /* g_signal_handlers_disconnect_by_func doesn't work if you - * have a metamarshal, meaning it doesn't work with - * soup_message_add_header_handler() - */ - g_signal_handlers_disconnect_matched (item->msg, G_SIGNAL_MATCH_DATA, - 0, 0, NULL, NULL, item); - g_signal_emit (session, signals[REQUEST_UNQUEUED], 0, item->msg); - soup_message_queue_item_unref (item); + item->state = SOUP_MESSAGE_CONNECTING; + + if (item->async) { + soup_message_queue_item_ref (item); + soup_connection_connect_async (item->conn, item->cancellable, + connect_async_complete, item); + return FALSE; + } else { + GError *error = NULL; + + soup_connection_connect_sync (item->conn, item->cancellable, &error); + connect_complete (item, conn, error); + + return TRUE; + } } void -soup_session_set_item_status (SoupSession *session, - SoupMessageQueueItem *item, - guint status_code) +soup_session_process_queue_item (SoupSession *session, + SoupMessageQueueItem *item, + gboolean *should_cleanup, + gboolean loop) { - SoupURI *uri; - char *msg; + g_assert (item->session == session); - switch (status_code) { - case SOUP_STATUS_CANT_RESOLVE: - case SOUP_STATUS_CANT_CONNECT: - uri = soup_message_get_uri (item->msg); - msg = g_strdup_printf ("%s (%s)", - soup_status_get_phrase (status_code), - uri->host); - soup_message_set_status_full (item->msg, status_code, msg); - g_free (msg); - break; + do { + if (item->paused) + return; - case SOUP_STATUS_CANT_RESOLVE_PROXY: - case SOUP_STATUS_CANT_CONNECT_PROXY: - if (item->proxy_uri && item->proxy_uri->host) { - msg = g_strdup_printf ("%s (%s)", - soup_status_get_phrase (status_code), - item->proxy_uri->host); - soup_message_set_status_full (item->msg, status_code, msg); - g_free (msg); + switch (item->state) { + case SOUP_MESSAGE_STARTING: + if (!get_connection (item, should_cleanup)) + return; break; + + case SOUP_MESSAGE_CONNECTED: + if (soup_connection_is_tunnelled (item->conn)) + tunnel_connect (item); + else + item->state = SOUP_MESSAGE_READY; + break; + + case SOUP_MESSAGE_READY: + if (item->msg->status_code) { + if (item->msg->status_code == SOUP_STATUS_TRY_AGAIN) { + soup_message_cleanup_response (item->msg); + item->state = SOUP_MESSAGE_STARTING; + } else + item->state = SOUP_MESSAGE_FINISHING; + break; + } + + item->state = SOUP_MESSAGE_RUNNING; + + soup_session_send_queue_item (session, item, message_completed); + + if (item->new_api) { + if (item->async) + async_send_request_running (session, item); + return; + } + break; + + case SOUP_MESSAGE_RUNNING: + if (item->async) + return; + + g_warn_if_fail (item->new_api); + item->state = SOUP_MESSAGE_FINISHING; + break; + + case SOUP_MESSAGE_CACHED: + /* Will be handled elsewhere */ + return; + + case SOUP_MESSAGE_RESTARTING: + item->state = SOUP_MESSAGE_STARTING; + soup_message_restarted (item->msg); + break; + + case SOUP_MESSAGE_FINISHING: + item->state = SOUP_MESSAGE_FINISHED; + soup_message_finished (item->msg); + if (item->state != SOUP_MESSAGE_FINISHED) { + g_return_if_fail (!item->new_api); + break; + } + + soup_message_queue_item_ref (item); + soup_session_unqueue_item (session, item); + if (item->async && item->callback) + item->callback (session, item->msg, item->callback_data); + soup_message_queue_item_unref (item); + return; + + default: + /* Nothing to do with this message in any + * other state. + */ + g_warn_if_fail (item->async); + return; } - soup_message_set_status (item->msg, status_code); - break; + } while (loop && item->state != SOUP_MESSAGE_FINISHED); +} - case SOUP_STATUS_SSL_FAILED: - if (!g_tls_backend_supports_tls (g_tls_backend_get_default ())) { - soup_message_set_status_full (item->msg, status_code, - "TLS/SSL support not available; install glib-networking"); - } else - soup_message_set_status (item->msg, status_code); - break; +static void +async_run_queue (SoupSession *session) +{ + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + SoupMessageQueueItem *item; + SoupMessage *msg; + gboolean try_cleanup = TRUE, should_cleanup = FALSE; - default: - soup_message_set_status (item->msg, status_code); - break; + g_object_ref (session); + soup_session_cleanup_connections (session, FALSE); + + try_again: + for (item = soup_message_queue_first (priv->queue); + item; + item = soup_message_queue_next (priv->queue, item)) { + msg = item->msg; + + /* CONNECT messages are handled specially */ + if (msg->method == SOUP_METHOD_CONNECT) + continue; + + if (item->async_context != soup_session_get_async_context (session)) + continue; + + soup_session_process_queue_item (session, item, &should_cleanup, TRUE); } + + if (try_cleanup && should_cleanup) { + /* There is at least one message in the queue that + * could be sent if we cleanupd an idle connection from + * some other server. + */ + if (soup_session_cleanup_connections (session, TRUE)) { + try_cleanup = should_cleanup = FALSE; + goto try_again; + } + } + + g_object_unref (session); +} + +static gboolean +idle_run_queue (gpointer user_data) +{ + SoupSessionPrivate *priv = user_data; + GSource *source; + + if (priv->disposed) + return FALSE; + + source = g_main_current_source (); + priv->run_queue_sources = g_slist_remove (priv->run_queue_sources, source); + + /* Ensure that the source is destroyed before running the queue */ + g_source_destroy (source); + g_source_unref (source); + + g_assert (priv->session); + async_run_queue (priv->session); + return FALSE; } /** @@ -1379,6 +2042,18 @@ soup_session_set_item_status (SoupSession *session, * qv. **/ +static void +soup_session_real_queue_message (SoupSession *session, SoupMessage *msg, + SoupSessionCallback callback, gpointer user_data) +{ + SoupMessageQueueItem *item; + + item = soup_session_append_queue_item (session, msg, TRUE, FALSE, + callback, user_data); + soup_session_kick_queue (session); + soup_message_queue_item_unref (item); +} + /** * soup_session_queue_message: * @session: a #SoupSession @@ -1387,14 +2062,26 @@ soup_session_set_item_status (SoupSession *session, * be called after the message completes or when an unrecoverable error occurs. * @user_data: (allow-none): a pointer passed to @callback. * - * Queues the message @msg for sending. All messages are processed - * while the glib main loop runs. If @msg has been processed before, - * any resources related to the time it was last sent are freed. + * Queues the message @msg for asynchronously sending the request and + * receiving a response in the current thread-default #GMainContext. + * If @msg has been processed before, any resources related to the + * time it was last sent are freed. * * Upon message completion, the callback specified in @callback will - * be invoked (in the thread associated with @session's async - * context). If after returning from this callback the message has not + * be invoked. If after returning from this callback the message has not * been requeued, @msg will be unreffed. + * + * (The behavior above applies to a plain #SoupSession; if you are + * using #SoupSessionAsync or #SoupSessionSync, then the #GMainContext + * that is used depends on the settings of #SoupSession:async-context + * and #SoupSession:use-thread-context, and for #SoupSessionSync, the + * message will actually be sent and processed in another thread, with + * only the final callback occurring in the indicated #GMainContext.) + * + * Contrast this method with soup_session_send_async(), which also + * asynchronously sends a message, but returns before reading the + * response body, and allows you to read the response via a + * #GInputStream. */ void soup_session_queue_message (SoupSession *session, SoupMessage *msg, @@ -1419,7 +2106,17 @@ soup_session_real_requeue_message (SoupSession *session, SoupMessage *msg) item = soup_message_queue_lookup (priv->queue, msg); g_return_if_fail (item != NULL); - item->state = SOUP_MESSAGE_RESTARTING; + + if (item->resend_count >= SOUP_SESSION_MAX_RESEND_COUNT) { + if (SOUP_STATUS_IS_REDIRECTION (msg->status_code)) + soup_message_set_status (msg, SOUP_STATUS_TOO_MANY_REDIRECTS); + else + g_warning ("SoupMessage %p stuck in infinite loop?", msg); + } else { + item->resend_count++; + item->state = SOUP_MESSAGE_RESTARTING; + } + soup_message_queue_item_unref (item); } @@ -1440,6 +2137,19 @@ soup_session_requeue_message (SoupSession *session, SoupMessage *msg) SOUP_SESSION_GET_CLASS (session)->requeue_message (session, msg); } +static guint +soup_session_real_send_message (SoupSession *session, SoupMessage *msg) +{ + SoupMessageQueueItem *item; + guint status; + + item = soup_session_append_queue_item (session, msg, FALSE, FALSE, + NULL, NULL); + soup_session_process_queue_item (session, item, NULL, TRUE); + status = msg->status_code; + soup_message_queue_item_unref (item); + return status; +} /** * soup_session_send_message: @@ -1450,7 +2160,18 @@ soup_session_requeue_message (SoupSession *session, SoupMessage *msg) * transfer is finished successfully or there is an unrecoverable * error. * - * @msg is not freed upon return. + * Unlike with soup_session_queue_message(), @msg is not freed upon + * return. + * + * (Note that if you call this method on a #SoupSessionAsync, it will + * still use asynchronous I/O internally, running the glib main loop + * to process the message, which may also cause other events to be + * processed.) + * + * Contrast this method with soup_session_send(), which also + * synchronously sends a message, but returns before reading the + * response body, and allows you to read the response via a + * #GInputStream. * * Return value: the HTTP status code of the response */ @@ -1471,6 +2192,9 @@ soup_session_send_message (SoupSession *session, SoupMessage *msg) * * Pauses HTTP I/O on @msg. Call soup_session_unpause_message() to * resume I/O. + * + * This may only be called for asynchronous messages (those sent on a + * #SoupSessionAsync or using soup_session_queue_message()). **/ void soup_session_pause_message (SoupSession *session, @@ -1485,6 +2209,7 @@ soup_session_pause_message (SoupSession *session, priv = SOUP_SESSION_GET_PRIVATE (session); item = soup_message_queue_lookup (priv->queue, msg); g_return_if_fail (item != NULL); + g_return_if_fail (item->async); item->paused = TRUE; if (item->state == SOUP_MESSAGE_RUNNING) @@ -1492,6 +2217,51 @@ soup_session_pause_message (SoupSession *session, soup_message_queue_item_unref (item); } +static void +soup_session_real_kick_queue (SoupSession *session) +{ + SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); + SoupMessageQueueItem *item; + gboolean have_sync_items = FALSE; + + if (priv->disposed) + return; + + for (item = soup_message_queue_first (priv->queue); + item; + item = soup_message_queue_next (priv->queue, item)) { + if (item->async) { + GSource *source; + + /* We use priv rather than session as the + * source data, because other parts of libsoup + * (or the calling app) may have sources using + * the session as the source data. + */ + source = g_main_context_find_source_by_user_data (item->async_context, priv); + if (!source) { + source = soup_add_completion_reffed (item->async_context, + idle_run_queue, priv); + priv->run_queue_sources = g_slist_prepend (priv->run_queue_sources, + source); + } + } else + have_sync_items = TRUE; + } + + if (have_sync_items) { + g_mutex_lock (&priv->conn_lock); + g_cond_broadcast (&priv->conn_cond); + g_mutex_unlock (&priv->conn_lock); + } +} + +void +soup_session_kick_queue (SoupSession *session) +{ + SOUP_SESSION_GET_CLASS (session)->kick (session); +} + /** * soup_session_unpause_message: * @session: a #SoupSession @@ -1503,6 +2273,9 @@ soup_session_pause_message (SoupSession *session, * If @msg is being sent via blocking I/O, this will resume reading or * writing immediately. If @msg is using non-blocking I/O, then * reading or writing won't resume until you return to the main loop. + * + * This may only be called for asynchronous messages (those sent on a + * #SoupSessionAsync or using soup_session_queue_message()). **/ void soup_session_unpause_message (SoupSession *session, @@ -1517,13 +2290,14 @@ soup_session_unpause_message (SoupSession *session, priv = SOUP_SESSION_GET_PRIVATE (session); item = soup_message_queue_lookup (priv->queue, msg); g_return_if_fail (item != NULL); + g_return_if_fail (item->async); item->paused = FALSE; if (item->state == SOUP_MESSAGE_RUNNING) soup_message_io_unpause (msg); soup_message_queue_item_unref (item); - SOUP_SESSION_GET_CLASS (session)->kick (session); + soup_session_kick_queue (session); } @@ -1540,6 +2314,7 @@ soup_session_real_cancel_message (SoupSession *session, SoupMessage *msg, guint soup_message_set_status (msg, status_code); g_cancellable_cancel (item->cancellable); + soup_session_kick_queue (item->session); soup_message_queue_item_unref (item); } @@ -1555,7 +2330,7 @@ soup_session_real_cancel_message (SoupSession *session, SoupMessage *msg, guint * may call this at any time after handing @msg off to @session; if * @session has started sending the request but has not yet received * the complete response, then it will close the request's connection. - * Note that with non-idempotent requests (eg, + * Note that with requests that have side effects (eg, * <literal>POST</literal>, <literal>PUT</literal>, * <literal>DELETE</literal>) it is possible that you might cancel the * request after the server acts on it, but before it returns a @@ -1566,9 +2341,12 @@ soup_session_real_cancel_message (SoupSession *session, SoupMessage *msg, guint * The response headers, on the other hand, will always be either * empty or complete. * - * For messages queued with soup_session_queue_message() (and - * cancelled from the same thread), the callback will be invoked - * before soup_session_cancel_message() returns. + * Beware that with the deprecated #SoupSessionAsync, messages queued + * with soup_session_queue_message() will have their callbacks invoked + * before soup_session_cancel_message() returns. The plain + * #SoupSession does not have this behavior; cancelling an + * asynchronous message will merely queue its callback to be run after + * returning to the main loop. **/ void soup_session_cancel_message (SoupSession *session, SoupMessage *msg, @@ -1599,20 +2377,66 @@ soup_session_real_flush_queue (SoupSession *session) { SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); SoupMessageQueueItem *item; + GHashTable *current = NULL; + gboolean done = FALSE; + + if (SOUP_IS_SESSION_SYNC (session)) { + /* Record the current contents of the queue */ + current = g_hash_table_new (NULL, NULL); + for (item = soup_message_queue_first (priv->queue); + item; + item = soup_message_queue_next (priv->queue, item)) + g_hash_table_insert (current, item, item); + } + /* Cancel everything */ for (item = soup_message_queue_first (priv->queue); item; item = soup_message_queue_next (priv->queue, item)) { soup_session_cancel_message (session, item->msg, SOUP_STATUS_CANCELLED); } + + if (SOUP_IS_SESSION_SYNC (session)) { + /* Wait until all of the items in @current have been + * removed from the queue. (This is not the same as + * "wait for the queue to be empty", because the app + * may queue new requests in response to the + * cancellation of the old ones. We don't try to + * cancel those requests as well, since we'd likely + * just end up looping forever.) + */ + g_mutex_lock (&priv->conn_lock); + do { + done = TRUE; + for (item = soup_message_queue_first (priv->queue); + item; + item = soup_message_queue_next (priv->queue, item)) { + if (g_hash_table_lookup (current, item)) + done = FALSE; + } + + if (!done) + g_cond_wait (&priv->conn_cond, &priv->conn_lock); + } while (!done); + g_mutex_unlock (&priv->conn_lock); + + g_hash_table_destroy (current); + } } /** * soup_session_abort: * @session: the session * - * Cancels all pending requests in @session. + * Cancels all pending requests in @session and closes all idle + * persistent connections. + * + * The message cancellation has the same semantics as with + * soup_session_cancel_message(); asynchronous requests on a + * #SoupSessionAsync will have their callback called before + * soup_session_abort() returns. Requests on a plain #SoupSession will + * not. **/ void soup_session_abort (SoupSession *session) @@ -1631,8 +2455,11 @@ soup_session_abort (SoupSession *session) g_mutex_lock (&priv->conn_lock); conns = NULL; g_hash_table_iter_init (&iter, priv->conns); - while (g_hash_table_iter_next (&iter, &conn, &host)) + while (g_hash_table_iter_next (&iter, &conn, &host)) { conns = g_slist_prepend (conns, g_object_ref (conn)); + g_hash_table_iter_remove (&iter); + drop_connection (session, host, conn); + } g_mutex_unlock (&priv->conn_lock); for (c = conns; c; c = c->next) { @@ -1666,23 +2493,19 @@ prefetch_uri (SoupSession *session, SoupURI *uri, } /** -* soup_session_prepare_for_uri: -* @session: a #SoupSession -* @uri: a #SoupURI which may be required -* -* Tells @session that @uri may be requested shortly, and so the -* session can try to prepare (resolving the domain name, obtaining -* proxy address, etc.) in order to work more quickly once the URI is -* actually requested. -* -* This method acts asynchronously, in @session's -* #SoupSession:async_context. If you are using #SoupSessionSync and do -* not have a main loop running, then you can't use this method. -* -* Since: 2.30 -* -* Deprecated: 2.38: use soup_session_prefetch_dns() instead -**/ + * soup_session_prepare_for_uri: + * @session: a #SoupSession + * @uri: a #SoupURI which may be required + * + * Tells @session that @uri may be requested shortly, and so the + * session can try to prepare (resolving the domain name, obtaining + * proxy address, etc.) in order to work more quickly once the URI is + * actually requested. + * + * Since: 2.30 + * + * Deprecated: 2.38: use soup_session_prefetch_dns() instead + **/ void soup_session_prepare_for_uri (SoupSession *session, SoupURI *uri) { @@ -1713,10 +2536,6 @@ soup_session_prepare_for_uri (SoupSession *session, SoupURI *uri) * resolution. @callback will still be invoked in this case, with a * status of %SOUP_STATUS_CANCELLED. * -* This method acts asynchronously, in @session's -* #SoupSession:async_context. If you are using #SoupSessionSync and do -* not have a main loop running, then you can't use this method. -* * Since: 2.38 **/ void @@ -1748,6 +2567,9 @@ soup_session_prefetch_dns (SoupSession *session, const char *hostname, * feature to the session at construct time by using the * %SOUP_SESSION_ADD_FEATURE property. * + * Note that a #SoupContentDecoder is added to the session by default + * (unless you are using one of the deprecated session subclasses). + * * Since: 2.24 **/ void @@ -1759,6 +2581,15 @@ soup_session_add_feature (SoupSession *session, SoupSessionFeature *feature) g_return_if_fail (SOUP_IS_SESSION_FEATURE (feature)); priv = SOUP_SESSION_GET_PRIVATE (session); + + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + if (SOUP_IS_PROXY_URI_RESOLVER (feature)) { + set_proxy_resolver (session, NULL, + SOUP_PROXY_URI_RESOLVER (feature), + NULL); + } + G_GNUC_END_IGNORE_DEPRECATIONS; + priv->features = g_slist_prepend (priv->features, g_object_ref (feature)); g_hash_table_remove_all (priv->features_cache); soup_session_feature_attach (feature, session); @@ -1774,29 +2605,45 @@ soup_session_add_feature (SoupSession *session, SoupSessionFeature *feature) * adds it to @session as with soup_session_add_feature(). You can use * this when you don't need to customize the new feature in any way. * - * If @feature_type is not a #SoupSessionFeature type, this gives - * each existing feature on @session the chance to accept @feature_type - * as a "subfeature". This can be used to add new #SoupAuth types, - * for instance. + * If @feature_type is not a #SoupSessionFeature type, this gives each + * existing feature on @session the chance to accept @feature_type as + * a "subfeature". This can be used to add new #SoupAuth or + * #SoupRequest types, for instance. * * You can also add a feature to the session at construct time by * using the %SOUP_SESSION_ADD_FEATURE_BY_TYPE property. * + * Note that a #SoupContentDecoder is added to the session by default + * (unless you are using one of the deprecated session subclasses). + * * Since: 2.24 **/ void soup_session_add_feature_by_type (SoupSession *session, GType feature_type) { + SoupSessionPrivate *priv; + g_return_if_fail (SOUP_IS_SESSION (session)); + priv = SOUP_SESSION_GET_PRIVATE (session); + if (g_type_is_a (feature_type, SOUP_TYPE_SESSION_FEATURE)) { SoupSessionFeature *feature; feature = g_object_new (feature_type, NULL); soup_session_add_feature (session, feature); g_object_unref (feature); + } else if (g_type_is_a (feature_type, SOUP_TYPE_REQUEST)) { + SoupRequestClass *request_class; + int i; + + request_class = g_type_class_ref (feature_type); + for (i = 0; request_class->schemes[i]; i++) { + g_hash_table_insert (priv->request_types, + (char *)request_class->schemes[i], + GSIZE_TO_POINTER (feature_type)); + } } else { - SoupSessionPrivate *priv = SOUP_SESSION_GET_PRIVATE (session); GSList *f; for (f = priv->features; f; f = f->next) { @@ -1828,6 +2675,15 @@ soup_session_remove_feature (SoupSession *session, SoupSessionFeature *feature) priv->features = g_slist_remove (priv->features, feature); g_hash_table_remove_all (priv->features_cache); soup_session_feature_detach (feature, session); + + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + if (SOUP_IS_PROXY_URI_RESOLVER (feature)) { + if (SOUP_IS_PROXY_RESOLVER_WRAPPER (priv->proxy_resolver) && + SOUP_PROXY_RESOLVER_WRAPPER (priv->proxy_resolver)->soup_resolver == SOUP_PROXY_URI_RESOLVER (feature)) + g_clear_object (&priv->proxy_resolver); + } + G_GNUC_END_IGNORE_DEPRECATIONS; + g_object_unref (feature); } } @@ -1862,6 +2718,21 @@ soup_session_remove_feature_by_type (SoupSession *session, GType feature_type) goto restart; } } + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + if (g_type_is_a (feature_type, SOUP_TYPE_PROXY_URI_RESOLVER)) + priv->proxy_use_default = FALSE; + G_GNUC_BEGIN_IGNORE_DEPRECATIONS; + } else if (g_type_is_a (feature_type, SOUP_TYPE_REQUEST)) { + SoupRequestClass *request_class; + int i; + + request_class = g_type_class_peek (feature_type); + if (!request_class) + return; + for (i = 0; request_class->schemes[i]; i++) { + g_hash_table_remove (priv->request_types, + request_class->schemes[i]); + } } else { for (f = priv->features; f; f = f->next) { if (soup_session_feature_remove_feature (f->data, feature_type)) @@ -1872,6 +2743,49 @@ soup_session_remove_feature_by_type (SoupSession *session, GType feature_type) } /** + * soup_session_has_feature: + * @session: a #SoupSession + * @feature_type: the #GType of the class of features to check for + * + * Tests if @session has at a feature of type @feature_type (which can + * be the type of either a #SoupSessionFeature, or else a subtype of + * some class managed by another feature, such as #SoupAuth or + * #SoupRequest). + * + * Return value: %TRUE or %FALSE + * + * Since: 2.42 + **/ +gboolean +soup_session_has_feature (SoupSession *session, + GType feature_type) +{ + SoupSessionPrivate *priv; + GSList *f; + + g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE); + + priv = SOUP_SESSION_GET_PRIVATE (session); + + if (g_type_is_a (feature_type, SOUP_TYPE_SESSION_FEATURE)) { + for (f = priv->features; f; f = f->next) { + if (G_TYPE_CHECK_INSTANCE_TYPE (f->data, feature_type)) + return TRUE; + } + } else if (g_type_is_a (feature_type, SOUP_TYPE_REQUEST)) { + return g_hash_table_lookup (priv->request_types, + GSIZE_TO_POINTER (feature_type)) != NULL; + } else { + for (f = priv->features; f; f = f->next) { + if (soup_session_feature_has_feature (f->data, feature_type)) + return TRUE; + } + } + + return FALSE; +} + +/** * soup_session_get_features: * @session: a #SoupSession * @feature_type: the #GType of the class of features to get @@ -1982,12 +2896,15 @@ soup_session_class_init (SoupSessionClass *session_class) g_type_class_add_private (session_class, sizeof (SoupSessionPrivate)); /* virtual method definition */ + session_class->queue_message = soup_session_real_queue_message; + session_class->send_message = soup_session_real_send_message; session_class->requeue_message = soup_session_real_requeue_message; session_class->cancel_message = soup_session_real_cancel_message; - session_class->auth_required = soup_session_real_auth_required; session_class->flush_queue = soup_session_real_flush_queue; + session_class->kick = soup_session_real_kick_queue; /* virtual method override */ + object_class->constructor = soup_session_constructor; object_class->dispose = soup_session_dispose; object_class->finalize = soup_session_finalize; object_class->set_property = soup_session_set_property; @@ -2036,7 +2953,7 @@ soup_session_class_init (SoupSessionClass *session_class) * #SoupMessage::finished (and all of the other #SoupMessage * signals) may be invoked multiple times for a given message. * - * Since: 2.4.1 + * Since: 2.24 **/ signals[REQUEST_QUEUED] = g_signal_new ("request-queued", @@ -2044,7 +2961,7 @@ soup_session_class_init (SoupSessionClass *session_class) G_SIGNAL_RUN_FIRST, 0, /* FIXME? */ NULL, NULL, - _soup_marshal_NONE__OBJECT, + NULL, G_TYPE_NONE, 1, SOUP_TYPE_MESSAGE); @@ -2064,7 +2981,7 @@ soup_session_class_init (SoupSessionClass *session_class) G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (SoupSessionClass, request_started), NULL, NULL, - _soup_marshal_NONE__OBJECT_OBJECT, + NULL, G_TYPE_NONE, 2, SOUP_TYPE_MESSAGE, SOUP_TYPE_SOCKET); @@ -2079,7 +2996,7 @@ soup_session_class_init (SoupSessionClass *session_class) * #SoupSession::request_queued for a detailed description of the * message lifecycle within a session. * - * Since: 2.4.1 + * Since: 2.24 **/ signals[REQUEST_UNQUEUED] = g_signal_new ("request-unqueued", @@ -2087,7 +3004,7 @@ soup_session_class_init (SoupSessionClass *session_class) G_SIGNAL_RUN_FIRST, 0, /* FIXME? */ NULL, NULL, - _soup_marshal_NONE__OBJECT, + NULL, G_TYPE_NONE, 1, SOUP_TYPE_MESSAGE); @@ -2120,7 +3037,7 @@ soup_session_class_init (SoupSessionClass *session_class) G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (SoupSessionClass, authenticate), NULL, NULL, - _soup_marshal_NONE__OBJECT_OBJECT_BOOLEAN, + NULL, G_TYPE_NONE, 3, SOUP_TYPE_MESSAGE, SOUP_TYPE_AUTH, @@ -2143,7 +3060,7 @@ soup_session_class_init (SoupSessionClass *session_class) G_SIGNAL_RUN_FIRST, 0, NULL, NULL, - _soup_marshal_NONE__OBJECT, + NULL, G_TYPE_NONE, 1, /* SoupConnection is private, so we can't use * SOUP_TYPE_CONNECTION here. @@ -2167,7 +3084,7 @@ soup_session_class_init (SoupSessionClass *session_class) G_SIGNAL_RUN_FIRST, 0, NULL, NULL, - _soup_marshal_NONE__OBJECT, + NULL, G_TYPE_NONE, 1, /* SoupConnection is private, so we can't use * SOUP_TYPE_CONNECTION here. @@ -2177,10 +3094,30 @@ soup_session_class_init (SoupSessionClass *session_class) /* properties */ /** + * SoupSession:proxy-uri: + * + * A proxy to use for all http and https requests in this + * session. Setting this will clear the + * #SoupSession:proxy-resolver property, and remove any + * <type>SoupProxyURIResolver</type> features that have been + * added to the session. Setting this property will also + * cancel all currently pending messages. + * + * Note that #SoupSession will normally handle looking up the + * user's proxy settings for you; you should only use + * #SoupSession:proxy-uri if you need to override the user's + * normal proxy settings. + * + * Also note that this proxy will be used for + * <emphasis>all</emphasis> requests; even requests to + * <literal>localhost</literal>. If you need more control over + * proxies, you can create a #GSimpleProxyResolver and set the + * #SoupSession:proxy-resolver property. + */ + /** * SOUP_SESSION_PROXY_URI: * - * Alias for the #SoupSession:proxy-uri property. (The HTTP - * proxy to use for this session.) + * Alias for the #SoupSession:proxy-uri property, qv. **/ g_object_class_install_property ( object_class, PROP_PROXY_URI, @@ -2190,10 +3127,37 @@ soup_session_class_init (SoupSessionClass *session_class) SOUP_TYPE_URI, G_PARAM_READWRITE)); /** + * SoupSession:proxy-resolver: + * + * A #GProxyResolver to use with this session. Setting this + * will clear the #SoupSession:proxy-uri property, and remove + * any <type>SoupProxyURIResolver</type> features that have + * been added to the session. + * + * By default, in a plain #SoupSession, this is set to the + * default #GProxyResolver, but you can set it to %NULL if you + * don't want to use proxies, or set it to your own + * #GProxyResolver if you want to control what proxies get + * used. + * + * Since: 2.42 + */ + /** + * SOUP_SESSION_PROXY_RESOLVER: + * + * Alias for the #SoupSession:proxy-resolver property, qv. + **/ + g_object_class_install_property ( + object_class, PROP_PROXY_RESOLVER, + g_param_spec_object (SOUP_SESSION_PROXY_RESOLVER, + "Proxy Resolver", + "The GProxyResolver to use for this session", + G_TYPE_PROXY_RESOLVER, + G_PARAM_READWRITE)); + /** * SOUP_SESSION_MAX_CONNS: * - * Alias for the #SoupSession:max-conns property. (The maximum - * number of connections that the session can open at once.) + * Alias for the #SoupSession:max-conns property, qv. **/ g_object_class_install_property ( object_class, PROP_MAX_CONNS, @@ -2207,9 +3171,7 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_MAX_CONNS_PER_HOST: * - * Alias for the #SoupSession:max-conns-per-host property. - * (The maximum number of connections that the session can - * open at once to a given host.) + * Alias for the #SoupSession:max-conns-per-host property, qv. **/ g_object_class_install_property ( object_class, PROP_MAX_CONNS_PER_HOST, @@ -2223,24 +3185,35 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SoupSession:idle-timeout: * - * Connection lifetime when idle + * Connection lifetime (in seconds) when idle. Any connection + * left idle longer than this will be closed. * - * Since: 2.4.1 + * Although you can change this property at any time, it will + * only affect newly-created connections, not currently-open + * ones. You can call soup_session_abort() after setting this + * if you want to ensure that all future connections will have + * this timeout value. + * + * Note that the default value of 60 seconds only applies to + * plain #SoupSessions. If you are using #SoupSessionAsync or + * #SoupSessionSync, the default value is 0 (meaning idle + * connections will never time out). + * + * Since: 2.24 **/ /** * SOUP_SESSION_IDLE_TIMEOUT: * - * Alias for the #SoupSession:idle-timeout property. (The idle - * connection lifetime.) + * Alias for the #SoupSession:idle-timeout property, qv. * - * Since: 2.4.1 + * Since: 2.24 **/ g_object_class_install_property ( object_class, PROP_IDLE_TIMEOUT, g_param_spec_uint (SOUP_SESSION_IDLE_TIMEOUT, "Idle Timeout", "Connection lifetime when idle", - 0, G_MAXUINT, 0, + 0, G_MAXUINT, 60, G_PARAM_READWRITE)); /** * SoupSession:use-ntlm: @@ -2253,8 +3226,7 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_USE_NTLM: * - * Alias for the #SoupSession:use-ntlm property. (Whether or - * not to use NTLM authentication.) + * Alias for the #SoupSession:use-ntlm property, qv. **/ g_object_class_install_property ( object_class, PROP_USE_NTLM, @@ -2262,23 +3234,25 @@ soup_session_class_init (SoupSessionClass *session_class) "Use NTLM", "Whether or not to use NTLM authentication", FALSE, - G_PARAM_READWRITE)); + G_PARAM_READWRITE | G_PARAM_DEPRECATED)); /** * SoupSession:ssl-ca-file: * * File containing SSL CA certificates. * - * Deprecated: use #SoupSession:ssl-use-system-ca-file or - * #SoupSession:tls-database instead + * If the specified file does not exist or cannot be read, + * then libsoup will print a warning, and then behave as + * though it had read in a empty CA file, meaning that all SSL + * certificates will be considered invalid. + * + * Deprecated: use #SoupSession:ssl-use-system-ca-file, or + * else #SoupSession:tls-database with a #GTlsFileDatabase + * (which allows you to do explicit error handling). **/ /** * SOUP_SESSION_SSL_CA_FILE: * - * Alias for the #SoupSession:ssl-ca-file property. (File - * containing SSL CA certificates.). - * - * Deprecated: use #SoupSession:ssl-use-system-ca-file or - * #SoupSession:tls-database instead + * Alias for the #SoupSession:ssl-ca-file property, qv. **/ g_object_class_install_property ( object_class, PROP_SSL_CA_FILE, @@ -2286,9 +3260,9 @@ soup_session_class_init (SoupSessionClass *session_class) "SSL CA file", "File containing SSL CA certificates", NULL, - G_PARAM_READWRITE)); + G_PARAM_READWRITE | G_PARAM_DEPRECATED)); /** - * SOUP_SESSION_USE_SYSTEM_CA_FILE: + * SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE: * * Alias for the #SoupSession:ssl-use-system-ca-file property, * qv. @@ -2310,6 +3284,11 @@ soup_session_class_init (SoupSessionClass *session_class) * See #SoupSession:ssl-strict for more information on how * https certificate validation is handled. * + * Note that the default value of %TRUE only applies to plain + * #SoupSessions. If you are using #SoupSessionAsync or + * #SoupSessionSync, the default value is %FALSE, for backward + * compatibility. + * * Since: 2.38 **/ g_object_class_install_property ( @@ -2317,7 +3296,7 @@ soup_session_class_init (SoupSessionClass *session_class) g_param_spec_boolean (SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, "Use system CA file", "Use the system certificate database", - FALSE, + TRUE, G_PARAM_READWRITE)); /** * SOUP_SESSION_TLS_DATABASE: @@ -2340,6 +3319,12 @@ soup_session_class_init (SoupSessionClass *session_class) * See #SoupSession:ssl-strict for more information on how * https certificate validation is handled. * + * If you are using a plain #SoupSession then + * #SoupSession:ssl-use-system-ca-file will be %TRUE by + * default, and so this property will be a copy of the system + * CA database. If you are using #SoupSessionAsync or + * #SoupSessionSync, this property will be %NULL by default. + * * Since: 2.38 **/ g_object_class_install_property ( @@ -2359,8 +3344,8 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SoupSession:ssl-strict: * - * Normally, if #SoupSession:tlsdb is set (including if it was - * set via #SoupSession:ssl-use-system-ca-file or + * Normally, if #SoupSession:tls-database is set (including if + * it was set via #SoupSession:ssl-use-system-ca-file or * #SoupSession:ssl-ca-file), then libsoup will reject any * certificate that is invalid (ie, expired) or that is not * signed by one of the given CA certificates, and the @@ -2374,9 +3359,13 @@ soup_session_class_init (SoupSessionClass *session_class) * accept invalid certificates after giving some sort of * warning.) * - * If the session has no CA file or TLS database, then all - * certificates are always accepted, and this property has no - * effect. + * For a plain #SoupSession, if the session has no CA file or + * TLS database, and this property is %TRUE, then all + * certificates will be rejected. However, beware that the + * deprecated #SoupSession subclasses (#SoupSessionAsync and + * #SoupSessionSync) have the opposite behavior: if there is + * no CA file or TLS database, then all certificates are always + * accepted, and this property has no effect. * * Since: 2.30 */ @@ -2388,10 +3377,26 @@ soup_session_class_init (SoupSessionClass *session_class) TRUE, G_PARAM_READWRITE)); /** + * SoupSession:async-context: + * + * The #GMainContext that miscellaneous session-related + * asynchronous callbacks are invoked on. (Eg, setting + * #SoupSession:idle-timeout will add a timeout source on this + * context.) + * + * For a plain #SoupSession, this property is always set to + * the #GMainContext that is the thread-default at the time + * the session was created, and cannot be overridden. For the + * deprecated #SoupSession subclasses, the default value is + * %NULL, meaning to use the global default #GMainContext. + * + * If #SoupSession:use-thread-context is %FALSE, this context + * will also be used for asynchronous HTTP I/O. + */ + /** * SOUP_SESSION_ASYNC_CONTEXT: * - * Alias for the #SoupSession:async-context property. (The - * session's #GMainContext.) + * Alias for the #SoupSession:async-context property, qv. */ g_object_class_install_property ( object_class, PROP_ASYNC_CONTEXT, @@ -2409,13 +3414,11 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SoupSession:use-thread-context: * - * If set, asynchronous operations in this session will run in + * If %TRUE (which it always is on a plain #SoupSession), + * asynchronous HTTP requests in this session will run in * whatever the thread-default #GMainContext is at the time - * they are started, rather than always occurring in a context - * fixed at the session's construction time. "Bookkeeping" - * tasks (like expiring idle connections) will happen in the - * context that was thread-default at the time the session was - * created. + * they are started, rather than always occurring in + * #SoupSession:async-context. * * Since: 2.38 */ @@ -2427,10 +3430,31 @@ soup_session_class_init (SoupSessionClass *session_class) FALSE, G_PARAM_READWRITE)); /** + * SoupSession:timeout: + * + * The timeout (in seconds) for socket I/O operations + * (including connecting to a server, and waiting for a reply + * to an HTTP request). + * + * Although you can change this property at any time, it will + * only affect newly-created connections, not currently-open + * ones. You can call soup_session_abort() after setting this + * if you want to ensure that all future connections will have + * this timeout value. + * + * Note that the default value of 60 seconds only applies to + * plain #SoupSessions. If you are using #SoupSessionAsync or + * #SoupSessionSync, the default value is 0 (meaning socket I/O + * will not time out). + * + * Not to be confused with #SoupSession:idle-timeout (which is + * the length of time that idle persistent connections will be + * kept open). + */ + /** * SOUP_SESSION_TIMEOUT: * - * Alias for the #SoupSession:timeout property. (The timeout - * in seconds for blocking socket I/O operations.) + * Alias for the #SoupSession:timeout property, qv. **/ g_object_class_install_property ( object_class, PROP_TIMEOUT, @@ -2544,8 +3568,7 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_ADD_FEATURE: (skip) * - * Alias for the #SoupSession:add-feature property. (Shortcut - * for calling soup_session_add_feature(). + * Alias for the #SoupSession:add-feature property, qv. * * Since: 2.24 **/ @@ -2567,8 +3590,7 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_ADD_FEATURE_BY_TYPE: (skip) * - * Alias for the #SoupSession:add-feature-by-type property. - * (Shortcut for calling soup_session_add_feature_by_type(). + * Alias for the #SoupSession:add-feature-by-type property, qv. * * Since: 2.24 **/ @@ -2577,7 +3599,7 @@ soup_session_class_init (SoupSessionClass *session_class) g_param_spec_gtype (SOUP_SESSION_ADD_FEATURE_BY_TYPE, "Add Feature By Type", "Add a feature object of the given type to the session", - SOUP_TYPE_SESSION_FEATURE, + G_TYPE_OBJECT, G_PARAM_READWRITE)); /** * SoupSession:remove-feature-by-type: (skip) @@ -2590,9 +3612,8 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_REMOVE_FEATURE_BY_TYPE: (skip) * - * Alias for the #SoupSession:remove-feature-by-type - * property. (Shortcut for calling - * soup_session_remove_feature_by_type(). + * Alias for the #SoupSession:remove-feature-by-type property, + * qv. * * Since: 2.24 **/ @@ -2601,7 +3622,7 @@ soup_session_class_init (SoupSessionClass *session_class) g_param_spec_gtype (SOUP_SESSION_REMOVE_FEATURE_BY_TYPE, "Remove Feature By Type", "Remove features of the given type from the session", - SOUP_TYPE_SESSION_FEATURE, + G_TYPE_OBJECT, G_PARAM_READWRITE)); /** * SoupSession:http-aliases: @@ -2611,14 +3632,14 @@ soup_session_class_init (SoupSessionClass *session_class) * <literal>"dav"</literal>, than a URI of * <literal>dav://example.com/path</literal> would be treated * identically to <literal>http://example.com/path</literal>. - * If the value is %NULL, then only "http" is recognized as - * meaning "http". * - * For backward-compatibility reasons, the default value for - * this property is an array containing the single element - * <literal>"*"</literal>, a special value which means that - * any scheme except "https" is considered to be an alias for - * "http". + * In a plain #SoupSession, the default value is %NULL, + * meaning that only "http" is recognized as meaning "http". + * In #SoupSessionAsync and #SoupSessionSync, for backward + * compatibility, the default value is an array containing the + * single element <literal>"*"</literal>, a special value + * which means that any scheme except "https" is considered to + * be an alias for "http". * * See also #SoupSession:https-aliases. * @@ -2627,8 +3648,7 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_HTTP_ALIASES: * - * Alias for the #SoupSession:http-aliases property. (URI - * schemes that will be considered aliases for "http".) + * Alias for the #SoupSession:http-aliases property, qv. * * Since: 2.38 */ @@ -2654,8 +3674,7 @@ soup_session_class_init (SoupSessionClass *session_class) /** * SOUP_SESSION_HTTPS_ALIASES: * - * Alias for the #SoupSession:https-aliases property. (URI - * schemes that will be considered aliases for "https".) + * Alias for the #SoupSession:https-aliases property, qv. * * Since: 2.38 **/ @@ -2666,4 +3685,874 @@ soup_session_class_init (SoupSessionClass *session_class) "URI schemes that are considered aliases for 'https'", G_TYPE_STRV, G_PARAM_READWRITE)); + + /** + * SOUP_SESSION_LOCAL_ADDRESS: + * + * Alias for the #SoupSession:local-address property, qv. + * + * Since: 2.42 + **/ + /** + * SoupSession:local-address: + * + * Sets the #SoupAddress to use for the client side of + * the connection. + * + * Use this property if you want for instance to bind the + * local socket to a specific IP address. + * + * Since: 2.42 + **/ + g_object_class_install_property ( + object_class, PROP_LOCAL_ADDRESS, + g_param_spec_object (SOUP_SESSION_LOCAL_ADDRESS, + "Local address", + "Address of local end of socket", + SOUP_TYPE_ADDRESS, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + + +static gboolean +expected_to_be_requeued (SoupSession *session, SoupMessage *msg) +{ + if (msg->status_code == SOUP_STATUS_UNAUTHORIZED || + msg->status_code == SOUP_STATUS_PROXY_UNAUTHORIZED) { + SoupSessionFeature *feature = + soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER); + return !feature || !soup_message_disables_feature (msg, feature); + } + + if (!(soup_message_get_flags (msg) & SOUP_MESSAGE_NO_REDIRECT)) + return soup_session_would_redirect (session, msg); + + return FALSE; +} + +/* send_request_async */ + +static void +async_send_request_return_result (SoupMessageQueueItem *item, + gpointer stream, GError *error) +{ + GTask *task; + + g_return_if_fail (item->task != NULL); + + g_signal_handlers_disconnect_matched (item->msg, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, item); + + task = item->task; + item->task = NULL; + + if (item->io_source) { + g_source_destroy (item->io_source); + g_clear_pointer (&item->io_source, g_source_unref); + } + + if (error) + g_task_return_error (task, error); + else if (item->error) { + if (stream) + g_object_unref (stream); + g_task_return_error (task, g_error_copy (item->error)); + } else if (SOUP_STATUS_IS_TRANSPORT_ERROR (item->msg->status_code)) { + if (stream) + g_object_unref (stream); + g_task_return_new_error (task, SOUP_HTTP_ERROR, + item->msg->status_code, + "%s", + item->msg->reason_phrase); + } else + g_task_return_pointer (task, stream, g_object_unref); + g_object_unref (task); +} + +static void +async_send_request_restarted (SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + + /* We won't be needing this, then. */ + g_object_set_data (G_OBJECT (item->msg), "SoupSession:ostream", NULL); + item->io_started = FALSE; +} + +static void +async_send_request_finished (SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + GMemoryOutputStream *mostream; + GInputStream *istream = NULL; + GError *error = NULL; + + if (!item->task) { + /* Something else already took care of it. */ + return; + } + + mostream = g_object_get_data (G_OBJECT (item->task), "SoupSession:ostream"); + if (mostream) { + gpointer data; + gssize size; + + /* We thought it would be requeued, but it wasn't, so + * return the original body. + */ + size = g_memory_output_stream_get_data_size (mostream); + data = size ? g_memory_output_stream_steal_data (mostream) : g_strdup (""); + istream = g_memory_input_stream_new_from_data (data, size, g_free); + } else if (item->io_started) { + /* The message finished before becoming readable. This + * will happen, eg, if it's cancelled from got-headers. + * Do nothing; the op will complete via read_ready_cb() + * after we return; + */ + return; + } else { + /* The message finished before even being started; + * probably a tunnel connect failure. + */ + istream = g_memory_input_stream_new (); + } + + async_send_request_return_result (item, istream, error); +} + +static void +send_async_spliced (GObject *source, GAsyncResult *result, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + GInputStream *istream = g_object_get_data (source, "istream"); + GError *error = NULL; + + /* It should be safe to call the sync close() method here since + * the message body has already been written. + */ + g_input_stream_close (istream, NULL, NULL); + g_object_unref (istream); + + /* If the message was cancelled, it will be completed via other means */ + if (g_cancellable_is_cancelled (item->cancellable) || + !item->task) { + soup_message_queue_item_unref (item); + return; + } + + if (g_output_stream_splice_finish (G_OUTPUT_STREAM (source), + result, &error) == -1) { + async_send_request_return_result (item, NULL, error); + soup_message_queue_item_unref (item); + return; + } + + /* Otherwise either restarted or finished will eventually be called. */ + soup_session_kick_queue (item->session); + soup_message_queue_item_unref (item); +} + +static void +send_async_maybe_complete (SoupMessageQueueItem *item, + GInputStream *stream) +{ + if (expected_to_be_requeued (item->session, item->msg)) { + GOutputStream *ostream; + + /* Gather the current message body... */ + ostream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free); + g_object_set_data_full (G_OBJECT (item->task), "SoupSession:ostream", + ostream, g_object_unref); + + g_object_set_data (G_OBJECT (ostream), "istream", stream); + + /* Give the splice op its own ref on item */ + soup_message_queue_item_ref (item); + /* We don't use CLOSE_SOURCE because we need to control when the + * side effects of closing the SoupClientInputStream happen. + */ + g_output_stream_splice_async (ostream, stream, + G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, + G_PRIORITY_DEFAULT, + item->cancellable, + send_async_spliced, item); + return; + } + + async_send_request_return_result (item, stream, NULL); +} + +static void try_run_until_read (SoupMessageQueueItem *item); + +static gboolean +read_ready_cb (SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + + g_clear_pointer (&item->io_source, g_source_unref); + try_run_until_read (item); + return FALSE; +} + +static void +try_run_until_read (SoupMessageQueueItem *item) +{ + GError *error = NULL; + GInputStream *stream = NULL; + + if (soup_message_io_run_until_read (item->msg, FALSE, item->cancellable, &error)) + stream = soup_message_io_get_response_istream (item->msg, &error); + if (stream) { + send_async_maybe_complete (item, stream); + return; + } + + if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_TRY_AGAIN)) { + item->state = SOUP_MESSAGE_RESTARTING; + soup_message_io_finished (item->msg); + g_error_free (error); + return; + } + + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { + if (item->state != SOUP_MESSAGE_FINISHED) { + if (soup_message_io_in_progress (item->msg)) + soup_message_io_finished (item->msg); + item->state = SOUP_MESSAGE_FINISHING; + soup_session_process_queue_item (item->session, item, NULL, FALSE); + } + async_send_request_return_result (item, NULL, error); + return; + } + + g_clear_error (&error); + item->io_source = soup_message_io_get_source (item->msg, item->cancellable, + read_ready_cb, item); + g_source_attach (item->io_source, soup_session_get_async_context (item->session)); +} + +static void +async_send_request_running (SoupSession *session, SoupMessageQueueItem *item) +{ + item->io_started = TRUE; + try_run_until_read (item); +} + +static void +async_return_from_cache (SoupMessageQueueItem *item, + GInputStream *stream) +{ + const char *content_type; + GHashTable *params = NULL; + + soup_message_got_headers (item->msg); + + content_type = soup_message_headers_get_content_type (item->msg->response_headers, ¶ms); + if (content_type) { + soup_message_content_sniffed (item->msg, content_type, params); + g_hash_table_unref (params); + } + + item->state = SOUP_MESSAGE_FINISHING; + async_send_request_return_result (item, g_object_ref (stream), NULL); +} + +typedef struct { + SoupCache *cache; + SoupMessage *conditional_msg; +} AsyncCacheCancelData; + + +static void +free_async_cache_cancel_data (AsyncCacheCancelData *data) +{ + g_object_unref (data->conditional_msg); + g_object_unref (data->cache); + g_slice_free (AsyncCacheCancelData, data); +} + +static void +cancel_cache_response (SoupMessageQueueItem *item) +{ + item->paused = FALSE; + item->state = SOUP_MESSAGE_FINISHING; + soup_message_set_status (item->msg, SOUP_STATUS_CANCELLED); + soup_session_kick_queue (item->session); +} + +static void +conditional_request_cancelled_cb (GCancellable *cancellable, AsyncCacheCancelData *data) +{ + soup_cache_cancel_conditional_request (data->cache, data->conditional_msg); +} + +static void +conditional_get_ready_cb (SoupSession *session, SoupMessage *msg, gpointer user_data) +{ + SoupMessageQueueItem *item = user_data; + GInputStream *stream; + SoupCache *cache; + + if (g_cancellable_is_cancelled (item->cancellable)) { + cancel_cache_response (item); + return; + } else { + gulong handler_id = GPOINTER_TO_SIZE (g_object_get_data (G_OBJECT (msg), "SoupSession:handler-id")); + g_cancellable_disconnect (item->cancellable, handler_id); + } + + cache = (SoupCache *)soup_session_get_feature (session, SOUP_TYPE_CACHE); + soup_cache_update_from_conditional_request (cache, msg); + + if (msg->status_code == SOUP_STATUS_NOT_MODIFIED) { + stream = soup_cache_send_response (cache, item->msg); + if (stream) { + async_return_from_cache (item, stream); + g_object_unref (stream); + return; + } + } + + /* The resource was modified or the server returned a 200 + * OK. Either way we reload it. FIXME. + */ + item->state = SOUP_MESSAGE_STARTING; + soup_session_kick_queue (session); +} + +static gboolean +idle_return_from_cache_cb (gpointer data) +{ + GTask *task = data; + SoupMessageQueueItem *item = g_task_get_task_data (task); + GInputStream *istream; + + if (item->state == SOUP_MESSAGE_FINISHED) { + /* The original request was cancelled using + * soup_session_cancel_message () so it has been + * already handled by the cancellation code path. + */ + return FALSE; + } else if (g_cancellable_is_cancelled (item->cancellable)) { + /* Cancel original msg after g_cancellable_cancel(). */ + cancel_cache_response (item); + return FALSE; + } + + istream = g_object_get_data (G_OBJECT (task), "SoupSession:istream"); + async_return_from_cache (item, istream); + + return FALSE; +} + + +static gboolean +async_respond_from_cache (SoupSession *session, + SoupMessageQueueItem *item) +{ + SoupCache *cache; + SoupCacheResponse response; + + cache = (SoupCache *)soup_session_get_feature (session, SOUP_TYPE_CACHE); + if (!cache) + return FALSE; + + response = soup_cache_has_response (cache, item->msg); + if (response == SOUP_CACHE_RESPONSE_FRESH) { + GInputStream *stream; + GSource *source; + + stream = soup_cache_send_response (cache, item->msg); + if (!stream) { + /* Cached file was deleted? */ + return FALSE; + } + g_object_set_data_full (G_OBJECT (item->task), "SoupSession:istream", + stream, g_object_unref); + + source = g_timeout_source_new (0); + g_task_attach_source (item->task, source, + (GSourceFunc) idle_return_from_cache_cb); + g_source_unref (source); + return TRUE; + } else if (response == SOUP_CACHE_RESPONSE_NEEDS_VALIDATION) { + SoupMessage *conditional_msg; + AsyncCacheCancelData *data; + gulong handler_id; + + conditional_msg = soup_cache_generate_conditional_request (cache, item->msg); + if (!conditional_msg) + return FALSE; + + /* Detect any quick cancellation before the cache is able to return data. */ + data = g_slice_new0 (AsyncCacheCancelData); + data->cache = g_object_ref (cache); + data->conditional_msg = g_object_ref (conditional_msg); + handler_id = g_cancellable_connect (item->cancellable, G_CALLBACK (conditional_request_cancelled_cb), + data, (GDestroyNotify) free_async_cache_cancel_data); + + g_object_set_data (G_OBJECT (conditional_msg), "SoupSession:handler-id", + GSIZE_TO_POINTER (handler_id)); + soup_session_queue_message (session, conditional_msg, + conditional_get_ready_cb, + item); + + + return TRUE; + } else + return FALSE; +} + +/** + * soup_session_send_async: + * @session: a #SoupSession + * @msg: a #SoupMessage + * @cancellable: a #GCancellable + * @callback: the callback to invoke + * @user_data: data for @callback + * + * Asynchronously sends @msg and waits for the beginning of a + * response. When @callback is called, then either @msg has been sent, + * and its response headers received, or else an error has occurred. + * Call soup_session_send_finish() to get a #GInputStream for reading + * the response body. + * + * See soup_session_send() for more details on the general semantics. + * + * Contrast this method with soup_session_queue_message(), which also + * asynchronously sends a #SoupMessage, but doesn't invoke its + * callback until the response has been completely read. + * + * (Note that this method cannot be called on the deprecated + * #SoupSessionSync subclass, and can only be called on + * #SoupSessionAsync if you have set the + * #SoupSession:use-thread-context property.) + * + * Since: 2.42 + */ +void +soup_session_send_async (SoupSession *session, + SoupMessage *msg, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SoupMessageQueueItem *item; + gboolean use_thread_context; + + g_return_if_fail (SOUP_IS_SESSION (session)); + g_return_if_fail (!SOUP_IS_SESSION_SYNC (session)); + + g_object_get (G_OBJECT (session), + SOUP_SESSION_USE_THREAD_CONTEXT, &use_thread_context, + NULL); + g_return_if_fail (use_thread_context); + + item = soup_session_append_queue_item (session, msg, TRUE, TRUE, + NULL, NULL); + g_signal_connect (msg, "restarted", + G_CALLBACK (async_send_request_restarted), item); + g_signal_connect (msg, "finished", + G_CALLBACK (async_send_request_finished), item); + + if (cancellable) { + g_object_unref (item->cancellable); + item->cancellable = g_object_ref (cancellable); + } + + item->new_api = TRUE; + item->task = g_task_new (session, item->cancellable, callback, user_data); + g_task_set_task_data (item->task, item, (GDestroyNotify) soup_message_queue_item_unref); + + /* Do not check for cancellations as we do not want to + * overwrite custom error messages set during cancellations + * (for example SOUP_HTTP_ERROR is set for cancelled messages + * in async_send_request_return_result() (status_code==1 + * means CANCEL and is considered a TRANSPORT_ERROR)). + */ + g_task_set_check_cancellable (item->task, FALSE); + + if (async_respond_from_cache (session, item)) + item->state = SOUP_MESSAGE_CACHED; + else + soup_session_kick_queue (session); +} + +/** + * soup_session_send_finish: + * @session: a #SoupSession + * @result: the #GAsyncResult passed to your callback + * @error: return location for a #GError, or %NULL + * + * Gets the response to a soup_session_send_async() call and (if + * successful), returns a #GInputStream that can be used to read the + * response body. + * + * Return value: (transfer full): a #GInputStream for reading the + * response body, or %NULL on error. + * + * Since: 2.42 + */ +GInputStream * +soup_session_send_finish (SoupSession *session, + GAsyncResult *result, + GError **error) +{ + GTask *task; + + g_return_val_if_fail (SOUP_IS_SESSION (session), NULL); + g_return_val_if_fail (!SOUP_IS_SESSION_SYNC (session), NULL); + g_return_val_if_fail (g_task_is_valid (result, session), NULL); + + task = G_TASK (result); + if (g_task_had_error (task)) { + SoupMessageQueueItem *item = g_task_get_task_data (task); + + if (soup_message_io_in_progress (item->msg)) + soup_message_io_finished (item->msg); + else if (item->state != SOUP_MESSAGE_FINISHED) + item->state = SOUP_MESSAGE_FINISHING; + + if (item->state != SOUP_MESSAGE_FINISHED) + soup_session_process_queue_item (session, item, NULL, FALSE); + } + + return g_task_propagate_pointer (task, error); +} + +/** + * soup_session_send: + * @session: a #SoupSession + * @msg: a #SoupMessage + * @cancellable: a #GCancellable + * @error: return location for a #GError, or %NULL + * + * Synchronously sends @msg and waits for the beginning of a response. + * On success, a #GInputStream will be returned which you can use to + * read the response body. ("Success" here means only that an HTTP + * response was received and understood; it does not necessarily mean + * that a 2xx class status code was received.) + * + * If non-%NULL, @cancellable can be used to cancel the request; + * soup_session_send() will return a %G_IO_ERROR_CANCELLED error. Note + * that with requests that have side effects (eg, + * <literal>POST</literal>, <literal>PUT</literal>, + * <literal>DELETE</literal>) it is possible that you might cancel the + * request after the server acts on it, but before it returns a + * response, leaving the remote resource in an unknown state. + * + * If @msg is requeued due to a redirect or authentication, the + * initial (3xx/401/407) response body will be suppressed, and + * soup_session_send() will only return once a final response has been + * received. + * + * Contrast this method with soup_session_send_message(), which also + * synchronously sends a #SoupMessage, but doesn't return until the + * response has been completely read. + * + * (Note that this method cannot be called on the deprecated + * #SoupSessionAsync subclass.) + * + * Return value: (transfer full): a #GInputStream for reading the + * response body, or %NULL on error. + * + * Since: 2.42 + */ +GInputStream * +soup_session_send (SoupSession *session, + SoupMessage *msg, + GCancellable *cancellable, + GError **error) +{ + SoupMessageQueueItem *item; + GInputStream *stream = NULL; + GOutputStream *ostream; + GMemoryOutputStream *mostream; + gssize size; + GError *my_error = NULL; + + g_return_val_if_fail (SOUP_IS_SESSION (session), NULL); + g_return_val_if_fail (!SOUP_IS_SESSION_ASYNC (session), NULL); + + item = soup_session_append_queue_item (session, msg, FALSE, TRUE, + NULL, NULL); + + item->new_api = TRUE; + if (cancellable) { + g_object_unref (item->cancellable); + item->cancellable = g_object_ref (cancellable); + } + + while (!stream) { + /* Get a connection, etc */ + soup_session_process_queue_item (session, item, NULL, TRUE); + if (item->state != SOUP_MESSAGE_RUNNING) + break; + + /* Send request, read headers */ + if (!soup_message_io_run_until_read (msg, TRUE, item->cancellable, &my_error)) { + if (g_error_matches (my_error, SOUP_HTTP_ERROR, SOUP_STATUS_TRY_AGAIN)) { + item->state = SOUP_MESSAGE_RESTARTING; + soup_message_io_finished (item->msg); + g_clear_error (&my_error); + continue; + } else + break; + } + + stream = soup_message_io_get_response_istream (msg, &my_error); + if (!stream) + break; + + if (!expected_to_be_requeued (session, msg)) + break; + + /* Gather the current message body... */ + ostream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free); + if (g_output_stream_splice (ostream, stream, + G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | + G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, + item->cancellable, &my_error) == -1) { + g_object_unref (stream); + g_object_unref (ostream); + stream = NULL; + break; + } + g_object_unref (stream); + stream = NULL; + + /* If the message was requeued, loop */ + if (item->state == SOUP_MESSAGE_RESTARTING) { + g_object_unref (ostream); + continue; + } + + /* Not requeued, so return the original body */ + mostream = G_MEMORY_OUTPUT_STREAM (ostream); + size = g_memory_output_stream_get_data_size (mostream); + stream = g_memory_input_stream_new (); + if (size) { + g_memory_input_stream_add_data (G_MEMORY_INPUT_STREAM (stream), + g_memory_output_stream_steal_data (mostream), + size, g_free); + } + g_object_unref (ostream); + } + + if (my_error) + g_propagate_error (error, my_error); + else if (item->error) { + g_clear_object (&stream); + if (error) + *error = g_error_copy (item->error); + } else if (SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code)) { + g_clear_object (&stream); + g_set_error_literal (error, SOUP_HTTP_ERROR, msg->status_code, + msg->reason_phrase); + } else if (!stream) + stream = g_memory_input_stream_new (); + + if (!stream) { + if (soup_message_io_in_progress (msg)) + soup_message_io_finished (msg); + else if (item->state != SOUP_MESSAGE_FINISHED) + item->state = SOUP_MESSAGE_FINISHING; + + if (item->state != SOUP_MESSAGE_FINISHED) + soup_session_process_queue_item (session, item, NULL, TRUE); + } + + soup_message_queue_item_unref (item); + return stream; +} + +/** + * soup_session_request: + * @session: a #SoupSession + * @uri_string: a URI, in string form + * @error: return location for a #GError, or %NULL + * + * Creates a #SoupRequest for retrieving @uri_string. + * + * Return value: (transfer full): a new #SoupRequest, or + * %NULL on error. + * + * Since: 2.42 + */ +SoupRequest * +soup_session_request (SoupSession *session, const char *uri_string, + GError **error) +{ + SoupURI *uri; + SoupRequest *req; + + uri = soup_uri_new (uri_string); + if (!uri) { + g_set_error (error, SOUP_REQUEST_ERROR, + SOUP_REQUEST_ERROR_BAD_URI, + _("Could not parse URI '%s'"), uri_string); + return NULL; + } + + req = soup_session_request_uri (session, uri, error); + soup_uri_free (uri); + return req; +} + +/** + * soup_session_request_uri: + * @session: a #SoupSession + * @uri: a #SoupURI representing the URI to retrieve + * @error: return location for a #GError, or %NULL + * + * Creates a #SoupRequest for retrieving @uri. + * + * Return value: (transfer full): a new #SoupRequest, or + * %NULL on error. + * + * Since: 2.42 + */ +SoupRequest * +soup_session_request_uri (SoupSession *session, SoupURI *uri, + GError **error) +{ + SoupSessionPrivate *priv; + GType request_type; + + g_return_val_if_fail (SOUP_IS_SESSION (session), NULL); + + priv = SOUP_SESSION_GET_PRIVATE (session); + + request_type = (GType)GPOINTER_TO_SIZE (g_hash_table_lookup (priv->request_types, uri->scheme)); + if (!request_type) { + g_set_error (error, SOUP_REQUEST_ERROR, + SOUP_REQUEST_ERROR_UNSUPPORTED_URI_SCHEME, + _("Unsupported URI scheme '%s'"), uri->scheme); + return NULL; + } + + return g_initable_new (request_type, NULL, error, + "uri", uri, + "session", session, + NULL); +} + +static SoupRequestHTTP * +initialize_http_request (SoupRequest *req, + const char *method, + GError **error) +{ + SoupRequestHTTP *http; + SoupMessage *msg; + + if (!SOUP_IS_REQUEST_HTTP (req)) { + g_object_unref (req); + g_set_error (error, SOUP_REQUEST_ERROR, + SOUP_REQUEST_ERROR_BAD_URI, + _("Not an HTTP URI")); + return NULL; + } + + http = SOUP_REQUEST_HTTP (req); + msg = soup_request_http_get_message (http); + g_object_set (G_OBJECT (msg), + SOUP_MESSAGE_METHOD, method, + NULL); + g_object_unref (msg); + + return http; +} + +/** + * soup_session_request_http: + * @session: a #SoupSession + * @method: an HTTP method + * @uri_string: a URI, in string form + * @error: return location for a #GError, or %NULL + * + * Creates a #SoupRequest for retrieving @uri_string, which must be an + * "http" or "https" URI (or another protocol listed in @session's + * #SoupSession:http-aliases or #SoupSession:https-aliases). + * + * Return value: (transfer full): a new #SoupRequestHTTP, or + * %NULL on error. + * + * Since: 2.42 + */ +SoupRequestHTTP * +soup_session_request_http (SoupSession *session, + const char *method, + const char *uri_string, + GError **error) +{ + SoupRequest *req; + + req = soup_session_request (session, uri_string, error); + if (!req) + return NULL; + + return initialize_http_request (req, method, error); +} + +/** + * soup_session_request_http_uri: + * @session: a #SoupSession + * @method: an HTTP method + * @uri: a #SoupURI representing the URI to retrieve + * @error: return location for a #GError, or %NULL + * + * Creates a #SoupRequest for retrieving @uri, which must be an + * "http" or "https" URI (or another protocol listed in @session's + * #SoupSession:http-aliases or #SoupSession:https-aliases). + * + * Return value: (transfer full): a new #SoupRequestHTTP, or + * %NULL on error. + * + * Since: 2.42 + */ +SoupRequestHTTP * +soup_session_request_http_uri (SoupSession *session, + const char *method, + SoupURI *uri, + GError **error) +{ + SoupRequest *req; + + req = soup_session_request_uri (session, uri, error); + if (!req) + return NULL; + + return initialize_http_request (req, method, error); +} + +/** + * SOUP_REQUEST_ERROR: + * + * A #GError domain for #SoupRequest<!-- -->-related errors. Used with + * #SoupRequestError. + * + * Since: 2.42 + */ +/** + * SoupRequestError: + * @SOUP_REQUEST_ERROR_BAD_URI: the URI could not be parsed + * @SOUP_REQUEST_ERROR_UNSUPPORTED_URI_SCHEME: the URI scheme is not + * supported by this #SoupSession + * @SOUP_REQUEST_ERROR_PARSING: the server's response could not + * be parsed + * @SOUP_REQUEST_ERROR_ENCODING: the server's response was in an + * unsupported format + * + * A #SoupRequest error. + * + * Since: 2.42 + */ + +GQuark +soup_request_error_quark (void) +{ + static GQuark error; + if (!error) + error = g_quark_from_static_string ("soup_request_error_quark"); + return error; } |