Allow the session authenticate signal to be handled asynchronously, by
authorDan Winship <danw@src.gnome.org>
Wed, 16 Jan 2008 21:49:54 +0000 (21:49 +0000)
committerDan Winship <danw@src.gnome.org>
Wed, 16 Jan 2008 21:49:54 +0000 (21:49 +0000)
* libsoup/soup-auth-manager.c (authorize_handler, etc): Allow the
session authenticate signal to be handled asynchronously, by
pausing the message and then authenticating the auth later.
(auth_type_compare_func): make this work. oops.
(extract_challenge): plug leak

* libsoup/soup-auth-manager-ntlm.c: Make this work async too.

* libsoup/soup-headers.c (soup_header_parse_list):
(soup_header_parse_param_list): plug leaks

* tests/auth-test.c (do_async_auth_test): test async auth

* docs/reference/client-howto.xml (Handling Authentication):
mention async auth

svn path=/trunk/; revision=1045

ChangeLog
docs/reference/client-howto.xml
libsoup/soup-auth-manager-ntlm.c
libsoup/soup-auth-manager.c
libsoup/soup-headers.c
libsoup/soup-session.c
tests/auth-test.c

index aa602d3..6482fc2 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,23 @@
 2008-01-16  Dan Winship  <danw@gnome.org>
 
+       * libsoup/soup-auth-manager.c (authorize_handler, etc): Allow the
+       session authenticate signal to be handled asynchronously, by
+       pausing the message and then authenticating the auth later.
+       (auth_type_compare_func): make this work. oops.
+       (extract_challenge): plug leak
+
+       * libsoup/soup-auth-manager-ntlm.c: Make this work async too.
+
+       * libsoup/soup-headers.c (soup_header_parse_list):
+       (soup_header_parse_param_list): plug leaks
+
+       * tests/auth-test.c (do_async_auth_test): test async auth
+
+       * docs/reference/client-howto.xml (Handling Authentication):
+       mention async auth
+
+2008-01-16  Dan Winship  <danw@gnome.org>
+
        * configure.in: Bomb out if glib 2.15.0 isn't found.
        (AM_PATH_GLIB_2_0 doesn't do this itself.)
 
index c52f635..23aa675 100644 (file)
@@ -345,6 +345,19 @@ at which point <application>libsoup</application> will allow the
 message to fail (with status 401 or 407).
 </para>
 
+<para>
+If you need to handle authentication asynchronously (eg, to pop up a
+password dialog without recursively entering the main loop), you can
+do that as well. Just call <link
+linkend="soup-session-pause-message"><function>soup_session_pause_message</function></link>
+on the message before returning from the signal handler, and
+<function>g_object_ref</function> the <type>SoupAuth</type>. Then,
+later on, after calling <function>soup_auth_authenticate</function>
+(or deciding not to), call <link
+linkend="soup-session-unpause-message"><function>soup_session_unpause_message</function></link>
+to resume the paused message.
+</para>
+
 </refsect2>
 
 <refsect2>
index b8bb7a6..59ea703 100644 (file)
@@ -33,6 +33,9 @@ typedef struct {
        SoupSocket *socket;
        SoupNTLMState state;
        char *response_header;
+
+       char *nonce, *domain;
+       SoupAuth *auth;
 } SoupNTLMConnection;
 
 struct SoupAuthManagerNTLM {
@@ -72,6 +75,10 @@ static void
 free_ntlm_connection (SoupNTLMConnection *conn)
 {
        g_free (conn->response_header);
+       g_free (conn->nonce);
+       g_free (conn->domain);
+       if (conn->auth)
+               g_object_unref (conn->auth);
        g_slice_free (SoupNTLMConnection, conn);
 }
 
@@ -161,11 +168,7 @@ ntlm_authorize_pre (SoupMessage *msg, gpointer user_data)
 {
        SoupAuthManagerNTLM *ntlm = user_data;
        SoupNTLMConnection *conn;
-       SoupAuth *auth;
        const char *val;
-       char *nonce;
-       const char *username = NULL, *password = NULL;
-       char *slash, *domain;
 
        conn = get_connection_for_msg (ntlm, msg);
        if (!conn)
@@ -189,38 +192,15 @@ ntlm_authorize_pre (SoupMessage *msg, gpointer user_data)
                goto done;
        }
 
-       if (!soup_ntlm_parse_challenge (val, &nonce, &domain)) {
+       if (!soup_ntlm_parse_challenge (val, &conn->nonce, &conn->domain)) {
                conn->state = SOUP_NTLM_FAILED;
                goto done;
        }
 
-       auth = soup_auth_ntlm_new (domain, soup_message_get_uri (msg)->host);
-       soup_session_emit_authenticate (ntlm->session, msg, auth, FALSE);
-       username = soup_auth_ntlm_get_username (auth);
-       password = soup_auth_ntlm_get_password (auth);
-       if (!username || !password) {
-               g_free (nonce);
-               g_free (domain);
-               g_object_unref (auth);
-               goto done;
-       }
-
-       slash = strpbrk (username, "\\/");
-       if (slash) {
-               g_free (domain);
-               domain = g_strdup (username);
-               slash = domain + (slash - username);
-               *slash = '\0';
-               username = slash + 1;
-       }
-
-       conn->response_header =
-               soup_ntlm_response (nonce, username, password, NULL, domain);
        conn->state = SOUP_NTLM_RECEIVED_CHALLENGE;
-
-       g_free (domain);
-       g_free (nonce);
-       g_object_unref (auth);
+       conn->auth = soup_auth_ntlm_new (conn->domain,
+                                        soup_message_get_uri (msg)->host);
+       soup_session_emit_authenticate (ntlm->session, msg, conn->auth, FALSE);
 
  done:
        /* Remove the WWW-Authenticate headers so the session won't try
@@ -234,14 +214,41 @@ ntlm_authorize_post (SoupMessage *msg, gpointer user_data)
 {
        SoupAuthManagerNTLM *ntlm = user_data;
        SoupNTLMConnection *conn;
+       const char *username = NULL, *password = NULL;
+       char *slash, *domain;
 
        conn = get_connection_for_msg (ntlm, msg);
-       if (!conn)
+       if (!conn || !conn->auth)
                return;
 
-       if (conn->state == SOUP_NTLM_RECEIVED_CHALLENGE &&
-           conn->response_header)
-               soup_session_requeue_message (ntlm->session, msg);
+       username = soup_auth_ntlm_get_username (conn->auth);
+       password = soup_auth_ntlm_get_password (conn->auth);
+       if (!username || !password)
+               goto done;
+
+       slash = strpbrk (username, "\\/");
+       if (slash) {
+               domain = g_strdup (username);
+               slash = domain + (slash - username);
+               *slash = '\0';
+               username = slash + 1;
+       } else
+               domain = conn->domain;
+
+       conn->response_header = soup_ntlm_response (conn->nonce,
+                                                   username, password,
+                                                   NULL, domain);
+       soup_session_requeue_message (ntlm->session, msg);
+
+done:
+       if (domain != conn->domain)
+               g_free (domain);
+       g_free (conn->domain);
+       conn->domain = NULL;
+       g_free (conn->nonce);
+       conn->nonce = NULL;
+       g_object_unref (conn->auth);
+       conn->auth = NULL;
 }
 
 static void
index 6417197..524cd9a 100644 (file)
@@ -99,10 +99,10 @@ soup_auth_manager_free (SoupAuthManager *manager)
 static int
 auth_type_compare_func (gconstpointer a, gconstpointer b)
 {
-       SoupAuthClass *auth1 = (SoupAuthClass *)a;
-       SoupAuthClass *auth2 = (SoupAuthClass *)b;
+       SoupAuthClass **auth1 = (SoupAuthClass **)a;
+       SoupAuthClass **auth2 = (SoupAuthClass **)b;
 
-       return auth2->strength - auth1->strength;
+       return (*auth2)->strength - (*auth1)->strength;
 }
 
 void
@@ -180,8 +180,10 @@ extract_challenge (const char *challenges, const char *scheme)
                    g_ascii_isspace (item[schemelen]))
                        break;
        }
-       if (!i)
+       if (!i) {
+               soup_header_free_list (items);
                return NULL;
+       }
 
        /* The challenge extends from this item until the end, or until
         * the next item that has a space before an equals sign.
@@ -315,9 +317,10 @@ authenticate_auth (SoupAuthManager *manager, SoupAuth *auth,
        return soup_auth_is_authenticated (auth);
 }
 
-static gboolean
-update_auth (SoupAuthManager *manager, SoupMessage *msg)
+static void
+update_auth (SoupMessage *msg, gpointer user_data)
 {
+       SoupAuthManager *manager = user_data;
        SoupAuthHost *host;
        SoupAuth *auth, *prior_auth, *old_auth;
        const char *path;
@@ -336,7 +339,7 @@ update_auth (SoupAuthManager *manager, SoupMessage *msg)
        } else {
                auth = create_auth (manager, msg);
                if (!auth)
-                       return FALSE;
+                       return;
        }
        auth_info = soup_auth_get_info (auth);
 
@@ -378,13 +381,24 @@ update_auth (SoupAuthManager *manager, SoupMessage *msg)
        }
 
        /* If we need to authenticate, try to do it. */
-       return authenticate_auth (manager, auth, msg,
-                                 prior_auth_failed, FALSE);
+       authenticate_auth (manager, auth, msg,
+                          prior_auth_failed, FALSE);
 }
 
-static gboolean
-update_proxy_auth (SoupAuthManager *manager, SoupMessage *msg)
+static void
+requeue_if_authenticated (SoupMessage *msg, gpointer user_data)
+{
+       SoupAuthManager *manager = user_data;
+       SoupAuth *auth = lookup_auth (manager, msg);
+
+       if (auth && soup_auth_is_authenticated (auth))
+               soup_session_requeue_message (manager->session, msg);
+}
+
+static void
+update_proxy_auth (SoupMessage *msg, gpointer user_data)
 {
+       SoupAuthManager *manager = user_data;
        SoupAuth *prior_auth;
        gboolean prior_auth_failed = FALSE;
 
@@ -398,29 +412,21 @@ update_proxy_auth (SoupAuthManager *manager, SoupMessage *msg)
        if (!manager->proxy_auth) {
                manager->proxy_auth = create_auth (manager, msg);
                if (!manager->proxy_auth)
-                       return FALSE;
+                       return;
        }
 
        /* If we need to authenticate, try to do it. */
-       return authenticate_auth (manager, manager->proxy_auth, msg,
-                                 prior_auth_failed, TRUE);
+       authenticate_auth (manager, manager->proxy_auth, msg,
+                          prior_auth_failed, TRUE);
 }
 
 static void
-authorize_handler (SoupMessage *msg, gpointer user_data)
+requeue_if_proxy_authenticated (SoupMessage *msg, gpointer user_data)
 {
        SoupAuthManager *manager = user_data;
+       SoupAuth *auth = manager->proxy_auth;
 
-       if (update_auth (manager, msg))
-               soup_session_requeue_message (manager->session, msg);
-}
-
-static void
-proxy_authorize_handler (SoupMessage *msg, gpointer user_data)
-{
-       SoupAuthManager *manager = user_data;
-
-       if (update_proxy_auth (manager, msg))
+       if (auth && soup_auth_is_authenticated (auth))
                soup_session_requeue_message (manager->session, msg);
 }
 
@@ -436,16 +442,20 @@ session_request_started (SoupSession *session, SoupMessage *msg,
                auth = NULL;
        soup_message_set_auth (msg, auth);
        soup_message_add_status_code_handler (
+               msg, "got_headers", SOUP_STATUS_UNAUTHORIZED,
+               G_CALLBACK (update_auth), manager);
+       soup_message_add_status_code_handler (
                msg, "got_body", SOUP_STATUS_UNAUTHORIZED,
-               G_CALLBACK (authorize_handler), manager);
+               G_CALLBACK (requeue_if_authenticated), manager);
 
        auth = manager->proxy_auth;
        if (!auth || !authenticate_auth (manager, auth, msg, FALSE, TRUE))
                auth = NULL;
        soup_message_set_proxy_auth (msg, auth);
        soup_message_add_status_code_handler (
+               msg, "got_headers", SOUP_STATUS_PROXY_UNAUTHORIZED,
+               G_CALLBACK (update_proxy_auth), manager);
+       soup_message_add_status_code_handler (
                msg, "got_body", SOUP_STATUS_PROXY_UNAUTHORIZED,
-               G_CALLBACK (proxy_authorize_handler), manager);
+               G_CALLBACK (requeue_if_proxy_authenticated), manager);
 }
-
-
index 1e69b02..3fe3993 100644 (file)
@@ -511,7 +511,7 @@ soup_header_free_list (GSList *list)
 
        for (l = list; l; l = l->next)
                g_free (l->data);
-       g_slist_free (l);
+       g_slist_free (list);
 }
 
 /**
@@ -609,6 +609,7 @@ soup_header_parse_param_list (const char *header)
                g_hash_table_insert (params, item, value);
        }
 
+       g_slist_free (list);
        return params;
 }
 
index 88156eb..07f1e2e 100644 (file)
@@ -257,6 +257,15 @@ soup_session_class_init (SoupSessionClass *session_class)
         * emitted again, with @retrying set to %TRUE, which will
         * continue until you return without calling
         * soup_auth_authenticate() on @auth.
+        *
+        * Note that this may be emitted before @msg's body has been
+        * fully read.
+        *
+        * If you call soup_session_pause_message() on @msg before
+        * returning, then you can authenticate @auth asynchronously
+        * (as long as you g_object_ref() it to make sure it doesn't
+        * get destroyed), and then unpause @msg when you are ready
+        * for it to continue.
         **/
        signals[AUTHENTICATE] =
                g_signal_new ("authenticate",
index 4ef5e3c..6b5d6a2 100644 (file)
@@ -352,6 +352,148 @@ do_digest_nonce_test (SoupSession *session,
        g_object_unref (msg);
 }
 
+/* Async auth test. We queue three requests to /Basic/realm1, ensuring
+ * that they are sent in order. The first and third ones will be
+ * paused from the authentication callback. The second will be allowed
+ * to fail. Shortly after the third one requests auth, we'll provide
+ * the auth and unpause the two remaining messages, allowing them to
+ * succeed.
+ */
+
+static void
+async_authenticate (SoupSession *session, SoupMessage *msg,
+                   SoupAuth *auth, gboolean retrying, gpointer data)
+{
+       SoupAuth **saved_auth = data;
+       int id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "id"));
+
+       debug_printf (2, "  async_authenticate msg%d\n", id);
+
+       /* The session will try to authenticate msg3 *before* sending
+        * it, because it already knows it's going to need the auth.
+        * Ignore that.
+        */
+       if (msg->status_code != SOUP_STATUS_UNAUTHORIZED) {
+               debug_printf (2, "    (ignoring)\n");
+               return;
+       }
+
+       soup_session_pause_message (session, msg);
+       if (saved_auth)
+               *saved_auth = g_object_ref (auth);
+       g_main_loop_quit (loop);
+}
+
+static void
+async_finished (SoupSession *session, SoupMessage *msg, gpointer user_data)
+{
+       int *finished = user_data;
+       int id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "id"));
+
+       debug_printf (2, "  async_finished msg%d\n", id);
+
+       (*finished)++;
+       if (*finished == 2)
+               g_main_loop_quit (loop);
+}
+
+static void
+do_async_auth_test (const char *base_uri)
+{
+       SoupSession *session;
+       SoupMessage *msg1, *msg2, *msg3, msg2_bak;
+       guint auth_id;
+       char *uri;
+       SoupAuth *auth = NULL;
+       int finished = 0;
+
+       debug_printf (1, "\nTesting async auth:\n");
+
+       session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
+
+       uri = g_strconcat (base_uri, "Basic/realm1/", NULL);
+
+       msg1 = soup_message_new ("GET", uri);
+       g_object_set_data (G_OBJECT (msg1), "id", GINT_TO_POINTER (1));
+       auth_id = g_signal_connect (session, "authenticate",
+                                   G_CALLBACK (async_authenticate), &auth);
+       g_object_ref (msg1);
+       soup_session_queue_message (session, msg1, async_finished, &finished);
+       g_main_loop_run (loop);
+       g_signal_handler_disconnect (session, auth_id);
+
+       /* async_authenticate will pause msg1 and quit loop */
+
+       msg2 = soup_message_new ("GET", uri);
+       g_object_set_data (G_OBJECT (msg2), "id", GINT_TO_POINTER (2));
+       soup_session_send_message (session, msg2);
+
+       if (msg2->status_code == SOUP_STATUS_UNAUTHORIZED)
+               debug_printf (1, "  msg2 failed as expected\n");
+       else {
+               debug_printf (1, "  msg2 got wrong status! (%u)\n",
+                             msg2->status_code);
+               errors++;
+       }
+
+       /* msg2 should be done at this point; assuming everything is
+        * working correctly, the session won't look at it again; we
+        * ensure that if it does, it will crash the test program.
+        */
+       memcpy (&msg2_bak, msg2, sizeof (SoupMessage));
+       memset (msg2, 0, sizeof (SoupMessage));
+
+       msg3 = soup_message_new ("GET", uri);
+       g_object_set_data (G_OBJECT (msg3), "id", GINT_TO_POINTER (3));
+       auth_id = g_signal_connect (session, "authenticate",
+                                   G_CALLBACK (async_authenticate), NULL);
+       g_object_ref (msg3);
+       soup_session_queue_message (session, msg3, async_finished, &finished);
+       g_main_loop_run (loop);
+       g_signal_handler_disconnect (session, auth_id);
+
+       /* async_authenticate will pause msg3 and quit loop */
+
+       /* Now do the auth, and restart */
+       if (auth) {
+               soup_auth_authenticate (auth, "user1", "realm1");
+               soup_session_unpause_message (session, msg1);
+               soup_session_unpause_message (session, msg3);
+
+               g_main_loop_run (loop);
+
+               /* async_finished will quit the loop */
+       } else {
+               debug_printf (1, "  msg1 didn't get authenticate signal!\n");
+               errors++;
+       }
+
+       if (msg1->status_code == SOUP_STATUS_OK)
+               debug_printf (1, "  msg1 succeeded\n");
+       else {
+               debug_printf (1, "  msg1 FAILED! (%u %s)\n",
+                             msg1->status_code, msg1->reason_phrase);
+               errors++;
+       }
+       if (msg3->status_code == SOUP_STATUS_OK)
+               debug_printf (1, "  msg3 succeeded\n");
+       else {
+               debug_printf (1, "  msg3 FAILED! (%u %s)\n",
+                             msg3->status_code, msg3->reason_phrase);
+               errors++;
+       }
+
+       soup_session_abort (session);
+       g_object_unref (session);
+
+       g_object_unref (msg1);
+       g_object_unref (msg3);
+       memcpy (msg2, &msg2_bak, sizeof (SoupMessage));
+       g_object_unref (msg2);
+       g_object_unref (auth);
+       g_free (uri);
+}
+
 int
 main (int argc, char **argv)
 {
@@ -417,6 +559,7 @@ main (int argc, char **argv)
        g_object_unref (session);
 
        /* And now for some regression tests */
+       loop = g_main_loop_new (NULL, TRUE);
 
        debug_printf (1, "Testing pipelined auth (bug 271540):\n");
        session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
@@ -437,8 +580,9 @@ main (int argc, char **argv)
        }
        g_free (uri);
 
-       loop = g_main_loop_new (NULL, TRUE);
        g_main_loop_run (loop);
+       soup_session_abort (session);
+       g_object_unref (session);
 
        debug_printf (1, "\nTesting digest nonce expiration:\n");
 
@@ -516,6 +660,8 @@ main (int argc, char **argv)
         *
         */
 
+       session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
+
        uri = g_strconcat (base_uri, "Digest/realm1/", NULL);
        do_digest_nonce_test (session, "First", uri, TRUE, TRUE);
        g_free (uri);
@@ -528,11 +674,14 @@ main (int argc, char **argv)
        do_digest_nonce_test (session, "Fourth", uri, FALSE, FALSE);
        g_free (uri);
 
-       g_main_loop_unref (loop);
-
        soup_session_abort (session);
        g_object_unref (session);
 
+       /* Async auth */
+       do_async_auth_test (base_uri);
+
+       g_main_loop_unref (loop);
+
        test_cleanup ();
        return errors != 0;
 }