These are all really SoupContext functions, so move them to soup-context.c libsoup-2-0-branch-base
authorDan Winship <danw@src.gnome.org>
Mon, 11 Aug 2003 13:50:51 +0000 (13:50 +0000)
committerDan Winship <danw@src.gnome.org>
Mon, 11 Aug 2003 13:50:51 +0000 (13:50 +0000)
* libsoup/soup-auth.c (soup_auth_lookup, soup_auth_set_context,
soup_auth_invalidate): These are all really SoupContext functions,
so move them to soup-context.c (and rename them appropriately).
(soup_auth_get_protection_space): New method to get the
"protection space" of an auth (paths where it is valid).
(soup_auth_invalidate): New method to try to un-authenticate an
auth (so we can keep the domain info cached even if the auth info
is wrong).
(basic_pspace_func): Basic protection space is all directories
below the current one.
(basic_invalidate_func): Clear the encoded username/password
(digest_pspace_func): Digest protection space is either the whole
server, or "what the domain parameter says" (though we don't deal
with cross-host domains).
(digest_invalidate_func): Return FALSE; bad digest auth info isn't
cacheable.
(digest_parse_func, digest_free): Set/free domain parameter
(ntlm_pspace): NTLM protection space is always the whole server.
(ntlm_invalidate): Clear the auth state.
(soup_auth_new_ntlm): Make this non-static
(SoupAuth): Replace the quad-state "status" field with an
"authenticated" boolean.

* libsoup/soup-private.h (SoupHost): Replace the "valid_auths"
hash with separate "auth_realms" (path->realm) and "auths"
(realm->auth) hashes. Also add a "use_ntlm" flag.

* libsoup/soup-context.c (soup_context_unref): Update SoupHost
freeing code.
(connection_free): Don't the connection's auth, just free it.
(soup_context_lookup_auth): Formerly soup_auth_lookup, but now
does two-stage lookup (path->realm then realm->auth) and also
deals with NTLM hacks.
(soup_context_update_auth): Mostly formerly soup_auth_set_context,
but also large parts of authorize_handler. Updates the auth hashes
based on information from a 401 or 407 response. Does a better job
than authorize_handler did of not throwing away good information.
(soup_context_preauthenticate): New; fakes up auth info so that
requests will end up using authentication without the server
needing to return an error first.
(soup_context_authenticate_auth): Moved out of authorize_handler
so it can be used at request-sending time too, if we know that we
need it. (That way we can avoid requeuing the request if it isn't
going to be able to be authenticated.)
(soup_context_invalidate_auth): Sort of like the old
soup_auth_invalidate, but only destroys the auth data, while still
remembering the path->realm mapping.

* libsoup/soup-message.c (authorize_handler): Mostly moved into
soup_context_update_auth.
(maybe_validate_auth): Remove this; it was only useful because of
bugs elsewhere in the auth handling.

* libsoup/soup-queue.c (soup_encode_http_auth): Update for
soup_context_lookup_auth. If the returned auth isn't
authenticated, call soup_context_authenticate_auth() on it.

* tests/auth-test.c: New (from soup-refactoring branch). Tests
that the Basic/Digest auth code does the right thing. (TODO: find
a good way to add NTLM tests too.)

* tests/Makefile.am (check_PROGRAMS): add auth-test

ChangeLog
libsoup/soup-auth.c
libsoup/soup-auth.h
libsoup/soup-context.c
libsoup/soup-context.h
libsoup/soup-message.c
libsoup/soup-private.h
libsoup/soup-queue.c
tests/.cvsignore
tests/Makefile.am
tests/auth-test.c [new file with mode: 0644]

index 94ab74e..7a69dde 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,68 @@
+2003-08-07  Dan Winship  <danw@ximian.com>
+
+       * libsoup/soup-auth.c (soup_auth_lookup, soup_auth_set_context,
+       soup_auth_invalidate): These are all really SoupContext functions,
+       so move them to soup-context.c (and rename them appropriately).
+       (soup_auth_get_protection_space): New method to get the
+       "protection space" of an auth (paths where it is valid).
+       (soup_auth_invalidate): New method to try to un-authenticate an
+       auth (so we can keep the domain info cached even if the auth info
+       is wrong).
+       (basic_pspace_func): Basic protection space is all directories
+       below the current one.
+       (basic_invalidate_func): Clear the encoded username/password
+       (digest_pspace_func): Digest protection space is either the whole
+       server, or "what the domain parameter says" (though we don't deal
+       with cross-host domains).
+       (digest_invalidate_func): Return FALSE; bad digest auth info isn't
+       cacheable.
+       (digest_parse_func, digest_free): Set/free domain parameter
+       (ntlm_pspace): NTLM protection space is always the whole server.
+       (ntlm_invalidate): Clear the auth state.
+       (soup_auth_new_ntlm): Make this non-static
+       (SoupAuth): Replace the quad-state "status" field with an
+       "authenticated" boolean.
+       
+       * libsoup/soup-private.h (SoupHost): Replace the "valid_auths"
+       hash with separate "auth_realms" (path->realm) and "auths"
+       (realm->auth) hashes. Also add a "use_ntlm" flag.
+
+       * libsoup/soup-context.c (soup_context_unref): Update SoupHost
+       freeing code.
+       (connection_free): Don't the connection's auth, just free it.
+       (soup_context_lookup_auth): Formerly soup_auth_lookup, but now
+       does two-stage lookup (path->realm then realm->auth) and also
+       deals with NTLM hacks.
+       (soup_context_update_auth): Mostly formerly soup_auth_set_context,
+       but also large parts of authorize_handler. Updates the auth hashes
+       based on information from a 401 or 407 response. Does a better job
+       than authorize_handler did of not throwing away good information.
+       (soup_context_preauthenticate): New; fakes up auth info so that
+       requests will end up using authentication without the server
+       needing to return an error first.
+       (soup_context_authenticate_auth): Moved out of authorize_handler
+       so it can be used at request-sending time too, if we know that we
+       need it. (That way we can avoid requeuing the request if it isn't
+       going to be able to be authenticated.)
+       (soup_context_invalidate_auth): Sort of like the old
+       soup_auth_invalidate, but only destroys the auth data, while still
+       remembering the path->realm mapping.
+
+       * libsoup/soup-message.c (authorize_handler): Mostly moved into
+       soup_context_update_auth.
+       (maybe_validate_auth): Remove this; it was only useful because of
+       bugs elsewhere in the auth handling.
+       
+       * libsoup/soup-queue.c (soup_encode_http_auth): Update for
+       soup_context_lookup_auth. If the returned auth isn't
+       authenticated, call soup_context_authenticate_auth() on it.
+
+       * tests/auth-test.c: New (from soup-refactoring branch). Tests
+       that the Basic/Digest auth code does the right thing. (TODO: find
+       a good way to add NTLM tests too.)
+
+       * tests/Makefile.am (check_PROGRAMS): add auth-test
+
 2003-07-29  Dan Winship  <danw@ximian.com>
 
        * configure.in: 1.99.25 ("Potato and Leek Soup")
index c8e3305..e9615eb 100644 (file)
@@ -62,6 +62,21 @@ basic_parse_func (SoupAuth *auth, const char *header)
        soup_header_param_destroy_hash (tokens);
 }
 
+static GSList *
+basic_pspace_func (SoupAuth *auth, const SoupUri *source_uri)
+{
+       char *space, *p;
+
+       space = g_strdup (source_uri->path);
+
+       /* Strip query and filename component */
+       p = strrchr (space, '/');
+       if (p && p != space && p[1])
+               *p = '\0';
+
+       return g_slist_prepend (NULL, space);
+}
+
 static void
 basic_init_func (SoupAuth *auth, const SoupUri *uri)
 {
@@ -71,6 +86,20 @@ basic_init_func (SoupAuth *auth, const SoupUri *uri)
        user_pass = g_strdup_printf ("%s:%s", uri->user, uri->passwd);
        basic->token = soup_base64_encode (user_pass, strlen (user_pass));
        g_free (user_pass);
+
+       auth->authenticated = TRUE;
+}
+
+static gboolean
+basic_invalidate_func (SoupAuth *auth)
+{
+       SoupAuthBasic *basic = (SoupAuthBasic *) auth;
+
+       g_free (basic->token);
+       basic->token = NULL;
+       auth->authenticated = FALSE;
+
+       return TRUE;
 }
 
 static void
@@ -91,9 +120,12 @@ soup_auth_new_basic (void)
        basic = g_new0 (SoupAuthBasic, 1);
        auth = (SoupAuth *) basic;
        auth->type = SOUP_AUTH_TYPE_BASIC;
+       auth->authenticated = FALSE;
 
        auth->parse_func = basic_parse_func;
        auth->init_func = basic_init_func;
+       auth->invalidate_func = basic_invalidate_func;
+       auth->pspace_func = basic_pspace_func;
        auth->auth_func = basic_auth_func;
        auth->free_func = basic_free;
 
@@ -126,6 +158,7 @@ typedef struct {
        char *nonce;
        QOPType qop_options;
        AlgorithmType algorithm;
+       char *domain;
 
        /* These are generated by the client */
        char *cnonce;
@@ -331,6 +364,7 @@ digest_parse_func (SoupAuth *auth, const char *header)
        auth->realm = soup_header_param_copy_token (tokens, "realm");
 
        digest->nonce = soup_header_param_copy_token (tokens, "nonce");
+       digest->domain = soup_header_param_copy_token (tokens, "domain");
 
        tmp = soup_header_param_copy_token (tokens, "qop");
        ptr = tmp;
@@ -356,6 +390,50 @@ digest_parse_func (SoupAuth *auth, const char *header)
        soup_header_param_destroy_hash (tokens);
 }
 
+static GSList *
+digest_pspace_func (SoupAuth *auth, const SoupUri *source_uri)
+{
+       SoupAuthDigest *digest = (SoupAuthDigest *) auth;
+       GSList *space = NULL;
+       SoupUri *uri;
+       char *domain, *d, *lasts, *dir, *slash;
+
+       if (!digest->domain || !*digest->domain) {
+               /* If no domain directive, the protection space is the
+                * whole server.
+                */
+               return g_slist_prepend (NULL, g_strdup (""));
+       }
+
+       domain = g_strdup (digest->domain);
+       for (d = strtok_r (domain, " ", &lasts); d; d = strtok_r (NULL, " ", &lasts)) {
+               if (*d == '/')
+                       dir = g_strdup (d);
+               else {
+                       uri = soup_uri_new (d);
+                       if (uri && uri->protocol == source_uri->protocol &&
+                           uri->port == source_uri->port &&
+                           !strcmp (uri->host, source_uri->host))
+                               dir = g_strdup (uri->path);
+                       else
+                               dir = NULL;
+                       if (uri)
+                               soup_uri_free (uri);
+               }
+
+               if (dir) {
+                       slash = strrchr (dir, '/');
+                       if (slash && !slash[1])
+                               *slash = '\0';
+
+                       space = g_slist_prepend (space, dir);
+               }
+       }
+       g_free (domain);
+
+       return space;
+}
+
 static void
 digest_init_func (SoupAuth *auth, const SoupUri *uri)
 {
@@ -392,6 +470,17 @@ digest_init_func (SoupAuth *auth, const SoupUri *uri)
        /* hexify A1 */
        md5_final (&ctx, d);
        digest_hex (d, digest->hex_a1);
+
+       auth->authenticated = TRUE;
+}
+
+static gboolean
+digest_invalidate_func (SoupAuth *auth)
+{
+       /* If we failed, we need to get a new nonce from the server
+        * next time, so this can't be reused.
+        */
+       return FALSE;
 }
 
 static void
@@ -400,6 +489,7 @@ digest_free (SoupAuth *auth)
        SoupAuthDigest *digest = (SoupAuthDigest *) auth;
 
        g_free (digest->user);
+       g_free (digest->domain);
 
        g_free (digest->nonce);
        g_free (digest->cnonce);
@@ -417,9 +507,12 @@ soup_auth_new_digest (void)
 
        auth = (SoupAuth *) digest;
        auth->type = SOUP_AUTH_TYPE_DIGEST;
+       auth->authenticated = FALSE;
 
        auth->parse_func = digest_parse_func;
        auth->init_func = digest_init_func;
+       auth->invalidate_func = digest_invalidate_func;
+       auth->pspace_func = digest_pspace_func;
        auth->auth_func = digest_auth_func;
        auth->free_func = digest_free;
 
@@ -452,15 +545,14 @@ static gchar *
 ntlm_auth (SoupAuth *sa, SoupMessage *msg)
 {
        SoupAuthNTLM *auth = (SoupAuthNTLM *) sa;
-       gchar *ret;
+       char *ret;
 
-       if (sa->status == SOUP_AUTH_STATUS_PENDING)
+       if (!sa->authenticated)
                return soup_ntlm_request ();
 
        /* Otherwise, return the response; but only once */
        ret = auth->response;
        auth->response = NULL;
-
        return ret;
 }
 
@@ -495,13 +587,20 @@ ntlm_parse (SoupAuth *sa, const char *header)
        g_strstrip (auth->header);
 }
 
+static GSList *
+ntlm_pspace (SoupAuth *auth, const SoupUri *source_uri)
+{
+       /* The protection space is the whole server. */
+       return g_slist_prepend (NULL, g_strdup (""));
+}
+
 static void
 ntlm_init (SoupAuth *sa, const SoupUri *uri)
 {
        SoupAuthNTLM *auth = (SoupAuthNTLM *) sa;
        gchar *host, *domain, *nonce;
 
-       if (strlen (auth->header) < sizeof ("NTLM"))
+       if (!auth->header || strlen (auth->header) < sizeof ("NTLM"))
                return;
 
        if (auth->response)
@@ -525,14 +624,24 @@ ntlm_init (SoupAuth *sa, const SoupUri *uri)
        g_free (host);
        g_free (domain);
 
-       /* Set this now so that if the server returns 401,
-        * soup will fail instead of looping (since that
-        * probably means the password was incorrect).
-        */
-       sa->status = SOUP_AUTH_STATUS_SUCCESSFUL;
+       g_free (auth->header);
+       auth->header = NULL;
+
+       sa->authenticated = TRUE;
+}
+
+static gboolean
+ntlm_invalidate (SoupAuth *sa)
+{
+       SoupAuthNTLM *auth = (SoupAuthNTLM *) sa;
 
+       g_free (auth->response);
+       auth->response = NULL;
        g_free (auth->header);
        auth->header = NULL;
+
+       sa->authenticated = FALSE;
+       return TRUE;
 }
 
 static void
@@ -545,16 +654,20 @@ ntlm_free (SoupAuth *sa)
        g_free (auth);
 }
 
-static SoupAuth *
-ntlm_new (void)
+SoupAuth *
+soup_auth_new_ntlm (void)
 {
        SoupAuthNTLM *auth;
 
        auth = g_new0 (SoupAuthNTLM, 1);
        auth->auth.type = SOUP_AUTH_TYPE_NTLM;
+       auth->auth.authenticated = FALSE;
+       auth->auth.realm = g_strdup ("");
 
        auth->auth.parse_func = ntlm_parse;
        auth->auth.init_func = ntlm_init;
+       auth->auth.invalidate_func = ntlm_invalidate;
+       auth->auth.pspace_func = ntlm_pspace;
        auth->auth.auth_func = ntlm_auth;
        auth->auth.free_func = ntlm_free;
 
@@ -566,92 +679,6 @@ ntlm_new (void)
  * Generic Authentication Interface
  */
 
-SoupAuth *
-soup_auth_lookup (SoupContext  *ctx)
-{
-       GHashTable *auth_hash = ctx->server->valid_auths;
-       SoupAuth *ret = NULL;
-       gchar *mypath, *dir;
-
-       if (!auth_hash) return NULL;
-
-       mypath = g_strdup (ctx->uri->path);
-       dir = mypath;
-
-        do {
-                ret = g_hash_table_lookup (auth_hash, mypath);
-                if (ret) break;
-
-                dir = strrchr (mypath, '/');
-                if (dir) *dir = '\0';
-        } while (dir);
-
-       g_free (mypath);
-       return ret;
-}
-
-void
-soup_auth_invalidate (SoupAuth *auth, SoupContext *ctx)
-{
-       SoupHost *server;
-       const SoupUri *uri;
-       SoupAuth *old_auth;
-       char *old_path;
-
-       g_return_if_fail (ctx != NULL);
-       g_return_if_fail (auth != NULL);
-
-       server = ctx->server;
-
-       if (!server->valid_auths)
-               return;
-
-       uri = soup_context_get_uri (ctx);
-       if (g_hash_table_lookup_extended (server->valid_auths,
-                                         uri->path,
-                                         (gpointer *) &old_path,
-                                         (gpointer *) &old_auth)) {
-               g_hash_table_remove (server->valid_auths, old_path);
-               g_free (old_path);
-               soup_auth_free (old_auth);
-       }
-}
-
-void
-soup_auth_set_context (SoupAuth *auth, SoupContext *ctx)
-{
-       SoupHost *server;
-       SoupAuth *old_auth = NULL;
-       gchar *old_path;
-       const SoupUri *uri;
-
-       g_return_if_fail (ctx != NULL);
-       g_return_if_fail (auth != NULL);
-
-       server = ctx->server;
-       uri = soup_context_get_uri (ctx);
-
-       if (!server->valid_auths) {
-               server->valid_auths = g_hash_table_new (g_str_hash, 
-                                                       g_str_equal);
-       }
-       else if (g_hash_table_lookup_extended (server->valid_auths, 
-                                              uri->path,
-                                              (gpointer *) &old_path,
-                                              (gpointer *) &old_auth)) {
-               if (auth == old_auth)
-                       return;
-
-               g_hash_table_remove (server->valid_auths, old_path);
-               g_free (old_path);
-               soup_auth_free (old_auth);
-       }
-
-       g_hash_table_insert (server->valid_auths,
-                            g_strdup (uri->path),
-                            auth);
-}
-
 typedef SoupAuth *(*SoupAuthNewFn) (void);
 
 typedef struct {
@@ -662,7 +689,7 @@ typedef struct {
 
 static AuthScheme known_auth_schemes [] = {
        { "Basic",  soup_auth_new_basic,  0 },
-       { "NTLM",   ntlm_new,             2 },
+       { "NTLM",   soup_auth_new_ntlm,   2 },
        { "Digest", soup_auth_new_digest, 3 },
        { NULL }
 };
@@ -727,6 +754,14 @@ soup_auth_initialize (SoupAuth *auth, const SoupUri *uri)
        auth->init_func (auth, uri);
 }
 
+gboolean
+soup_auth_invalidate (SoupAuth *auth)
+{
+       g_return_val_if_fail (auth != NULL, FALSE);
+
+       return auth->invalidate_func (auth);
+}
+
 gchar *
 soup_auth_authorize (SoupAuth *auth, SoupMessage *msg)
 {
@@ -744,3 +779,22 @@ soup_auth_free (SoupAuth *auth)
        g_free (auth->realm);
        auth->free_func (auth);
 }
+
+GSList *
+soup_auth_get_protection_space (SoupAuth *auth, const SoupUri *source_uri)
+{
+       g_return_val_if_fail (auth != NULL, NULL);
+       g_return_val_if_fail (source_uri != NULL, NULL);
+
+       return auth->pspace_func (auth, source_uri);
+}
+
+void
+soup_auth_free_protection_space (SoupAuth *auth, GSList *space)
+{
+       GSList *s;
+
+       for (s = space; s; s = s->next)
+               g_free (s->data);
+       g_slist_free (space);
+}
index e1c919d..756029e 100644 (file)
 typedef   enum _SoupAuthStatus SoupAuthStatus;
 typedef struct _SoupAuth       SoupAuth;
 
-enum _SoupAuthStatus {
-       SOUP_AUTH_STATUS_INVALID = 0,
-       SOUP_AUTH_STATUS_PENDING,
-       SOUP_AUTH_STATUS_FAILED,
-       SOUP_AUTH_STATUS_SUCCESSFUL
-};
-
 struct _SoupAuth {
-       SoupAuthType  type;
-       gchar        *realm;
+       SoupAuthType    type;
+       char           *realm;
+       gboolean        authenticated;
 
-       SoupAuthStatus status;
-       SoupMessage *controlling_msg;
+       void     (*parse_func)      (SoupAuth      *auth,
+                                    const gchar   *header);
 
-       void     (*parse_func)   (SoupAuth      *auth,
-                                 const gchar   *header);
+       void     (*init_func)       (SoupAuth      *auth, 
+                                    const SoupUri *uri);
 
-       void     (*init_func)    (SoupAuth      *auth, 
-                                 const SoupUri *uri);
+       gboolean (*invalidate_func) (SoupAuth      *auth);
 
-       char    *(*auth_func)    (SoupAuth      *auth, 
-                                 SoupMessage   *message);
+       char    *(*auth_func)       (SoupAuth      *auth, 
+                                    SoupMessage   *message);
 
-       void     (*free_func)    (SoupAuth      *auth);
+       GSList  *(*pspace_func)     (SoupAuth      *auth,
+                                    const SoupUri *source_uri);
+
+       void     (*free_func)       (SoupAuth      *auth);
 };
 
-SoupAuth *soup_auth_lookup                 (SoupContext   *ctx);
+SoupAuth   *soup_auth_new_from_header_list  (const SoupUri *uri,
+                                            const GSList  *header);
+
+SoupAuth   *soup_auth_new_ntlm              (void);
 
-void      soup_auth_set_context            (SoupAuth      *auth,
-                                           SoupContext   *ctx);
+void        soup_auth_initialize            (SoupAuth      *auth,
+                                            const SoupUri *uri);
 
-void      soup_auth_invalidate             (SoupAuth      *auth,
-                                           SoupContext   *ctx);
+gboolean    soup_auth_invalidate            (SoupAuth      *auth);
 
-SoupAuth *soup_auth_new_from_header_list   (const SoupUri *uri,
-                                           const GSList  *header);
+void        soup_auth_free                  (SoupAuth      *auth);
 
-void      soup_auth_initialize             (SoupAuth      *auth,
-                                           const SoupUri *uri);
+gchar      *soup_auth_authorize             (SoupAuth      *auth, 
+                                            SoupMessage   *msg);
 
-void      soup_auth_free                   (SoupAuth      *auth);
+GSList     *soup_auth_get_protection_space  (SoupAuth      *auth,
+                                            const SoupUri *source_uri);
+void        soup_auth_free_protection_space (SoupAuth      *auth,
+                                            GSList        *space);
 
-gchar    *soup_auth_authorize              (SoupAuth      *auth, 
-                                           SoupMessage   *msg);
 
 #endif /* SOUP_AUTH_H */
index e5dc6e9..4c4cd30 100644 (file)
@@ -193,13 +193,18 @@ soup_context_ref (SoupContext *ctx)
        ctx->refcnt++;
 }
 
-static gboolean
-remove_auth (gchar *path, SoupAuth *auth)
+static void
+free_path (gpointer path, gpointer realm, gpointer unused)
 {
        g_free (path);
-       soup_auth_free (auth);
+       g_free (realm);
+}
 
-       return TRUE;
+static void
+free_auth (gpointer realm, gpointer auth, gpointer unused)
+{
+       g_free (realm);
+       soup_auth_free (auth);
 }
 
 /**
@@ -231,12 +236,15 @@ soup_context_unref (SoupContext *ctx)
                        /* 
                         * Free all cached SoupAuths
                         */
-                       if (serv->valid_auths) {
-                               g_hash_table_foreach_remove (
-                                       serv->valid_auths,
-                                       (GHRFunc) remove_auth,
-                                       NULL);
-                               g_hash_table_destroy (serv->valid_auths);
+                       if (serv->auth_realms) {
+                               g_hash_table_foreach (serv->auth_realms,
+                                                     free_path, NULL);
+                               g_hash_table_destroy (serv->auth_realms);
+                       }
+                       if (serv->auths) {
+                               g_hash_table_foreach (serv->auths,
+                                                     free_auth, NULL);
+                               g_hash_table_destroy (serv->auths);
                        }
 
                        g_hash_table_destroy (serv->contexts);
@@ -257,10 +265,8 @@ connection_free (SoupConnection *conn)
        conn->server->connections =
                g_slist_remove (conn->server->connections, conn);
 
-       if (conn->auth) {
-               soup_auth_invalidate (conn->auth, conn->context);
+       if (conn->auth)
                soup_auth_free (conn->auth);
-       }
 
        g_io_channel_unref (conn->channel);
        soup_context_unref (conn->context);
@@ -770,3 +776,223 @@ soup_connection_purge_idle (void)
                connection_free (i->data);
        g_slist_free (idle_conns);
 }
+
+
+/* Authentication */
+
+SoupAuth *
+soup_context_lookup_auth (SoupContext *ctx, SoupMessage *msg)
+{
+       char *path, *dir;
+       const char *realm;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (ctx->server->use_ntlm && msg && msg->connection) {
+               if (!msg->connection->auth)
+                       msg->connection->auth = soup_auth_new_ntlm ();
+               return msg->connection->auth;
+       }
+
+       if (!ctx->server->auth_realms)
+               return NULL;
+
+       path = g_strdup (ctx->uri->path);
+       dir = path;
+        do {
+                realm = g_hash_table_lookup (ctx->server->auth_realms, path);
+                if (realm)
+                       break;
+
+                dir = strrchr (path, '/');
+                if (dir)
+                       *dir = '\0';
+        } while (dir);
+
+       g_free (path);
+       if (realm)
+               return g_hash_table_lookup (ctx->server->auths, realm);
+       else
+               return NULL;
+}
+
+static gboolean
+update_auth_internal (SoupContext *ctx, SoupConnection *conn,
+                     const GSList *headers, gboolean prior_auth_failed)
+{
+       SoupHost *server = ctx->server;
+       SoupAuth *new_auth, *prior_auth, *old_auth;
+       gpointer old_path, old_realm;
+       const char *path;
+       char *realm;
+       GSList *pspace, *p;
+
+       if (server->use_ntlm && conn && conn->auth) {
+               if (conn->auth->authenticated) {
+                       /* This is a "permission denied", not a
+                        * "password incorrect". There's nothing more
+                        * we can do.
+                        */
+                       return FALSE;
+               }
+
+               /* Free the intermediate auth */
+               soup_auth_free (conn->auth);
+               conn->auth = NULL;
+       }
+
+       /* Try to construct a new auth from the headers; if we can't,
+        * there's no way we'll be able to authenticate.
+        */
+       new_auth = soup_auth_new_from_header_list (ctx->uri, headers);
+       if (!new_auth)
+               return FALSE;
+
+       /* See if this auth is the same auth we used last time */
+       prior_auth = soup_context_lookup_auth (ctx, NULL);
+       if (prior_auth && prior_auth->type == new_auth->type &&
+           !strcmp (prior_auth->realm, new_auth->realm)) {
+               soup_auth_free (new_auth);
+               if (prior_auth_failed) {
+                       /* The server didn't like the username/password
+                        * we provided before.
+                        */
+                       soup_context_invalidate_auth (ctx, prior_auth);
+                       return FALSE;
+               } else {
+                       /* The user is trying to preauthenticate using
+                        * information we already have, so there's nothing
+                        * that needs to be done.
+                        */
+                       return TRUE;
+               }
+       }
+
+       if (new_auth->type == SOUP_AUTH_TYPE_NTLM) {
+               server->use_ntlm = TRUE;
+               if (conn) {
+                       conn->auth = new_auth;
+                       return soup_context_authenticate_auth (ctx, new_auth);
+               } else {
+                       soup_auth_free (new_auth);
+                       return FALSE;
+               }
+       }
+
+       if (!server->auth_realms) {
+               server->auth_realms = g_hash_table_new (g_str_hash, g_str_equal);
+               server->auths = g_hash_table_new (g_str_hash, g_str_equal);
+       }
+
+       /* Record where this auth realm is used */
+       realm = g_strdup_printf ("%d:%s", new_auth->type, new_auth->realm);
+       pspace = soup_auth_get_protection_space (new_auth, ctx->uri);
+       for (p = pspace; p; p = p->next) {
+               path = p->data;
+               if (g_hash_table_lookup_extended (server->auth_realms, path,
+                                                 &old_path, &old_realm)) {
+                       g_hash_table_remove (server->auth_realms, old_path);
+                       g_free (old_path);
+                       g_free (old_realm);
+               }
+
+               g_hash_table_insert (server->auth_realms,
+                                    g_strdup (path), g_strdup (realm));
+       }
+       soup_auth_free_protection_space (new_auth, pspace);
+
+       /* Now, make sure the auth is recorded. (If there's a
+        * pre-existing auth, we keep that rather than the new one,
+        * since the old one might already be authenticated.)
+        */
+       old_auth = g_hash_table_lookup (server->auths, realm);
+       if (old_auth) {
+               g_free (realm);
+               soup_auth_free (new_auth);
+               new_auth = old_auth;
+       } else 
+               g_hash_table_insert (server->auths, realm, new_auth);
+
+       /* Try to authenticate if needed. */
+       if (!new_auth->authenticated)
+               return soup_context_authenticate_auth (ctx, new_auth);
+
+       return TRUE;
+}
+
+gboolean
+soup_context_update_auth (SoupContext *ctx, SoupMessage *msg)
+{
+       const GSList *headers;
+
+       g_return_val_if_fail (ctx != NULL, FALSE);
+       g_return_val_if_fail (msg != NULL, FALSE);
+
+       if (msg->errorcode == SOUP_ERROR_PROXY_UNAUTHORIZED) {
+               headers = soup_message_get_header_list (msg->response_headers,
+                                                       "Proxy-Authenticate");
+       } else {
+               headers = soup_message_get_header_list (msg->response_headers,
+                                                       "WWW-Authenticate");
+       }
+
+       return update_auth_internal (ctx, msg->connection, headers, TRUE);
+}
+
+void
+soup_context_preauthenticate (SoupContext *ctx, const char *header)
+{
+       GSList *headers;
+
+       g_return_if_fail (ctx != NULL);
+       g_return_if_fail (header != NULL);
+
+       headers = g_slist_append (NULL, (char *)header);
+       update_auth_internal (ctx, NULL, headers, FALSE);
+       g_slist_free (headers);
+}
+
+gboolean
+soup_context_authenticate_auth (SoupContext *ctx, SoupAuth *auth)
+{
+       const SoupUri *uri = ctx->uri;
+
+       if (!uri->user && soup_auth_fn) {
+               (*soup_auth_fn) (auth->type,
+                                (SoupUri *) uri,
+                                auth->realm, 
+                                soup_auth_fn_user_data);
+       }
+
+       if (!uri->user)
+               return FALSE;
+
+       soup_auth_initialize (auth, uri);
+       return TRUE;
+}
+
+void
+soup_context_invalidate_auth (SoupContext *ctx, SoupAuth *auth)
+{
+       char *realm;
+       gpointer key, value;
+
+       g_return_if_fail (ctx != NULL);
+       g_return_if_fail (auth != NULL);
+
+       /* Try to just clean up the auth without removing it. */
+       if (soup_auth_invalidate (auth))
+               return;
+
+       /* Nope, need to remove it completely */
+       realm = g_strdup_printf ("%d:%s", auth->type, auth->realm);
+
+       if (g_hash_table_lookup_extended (ctx->server->auths, realm,
+                                         &key, &value) &&
+           auth == (SoupAuth *)value) {
+               g_hash_table_remove (ctx->server->auths, realm);
+               g_free (key);
+               soup_auth_free (auth);
+       }
+       g_free (realm);
+}
index 581dd84..79853d1 100644 (file)
@@ -65,4 +65,8 @@ void           soup_connection_release        (SoupConnection       *conn);
 
 void           soup_connection_purge_idle     (void);
 
+
+void           soup_context_preauthenticate   (SoupContext          *ctx,
+                                              const char           *header);
+                                         
 #endif /*SOUP_CONTEXT_H*/
index 92079c1..254394e 100644 (file)
@@ -8,6 +8,8 @@
  * Copyright (C) 2000-2002, Ximian, Inc.
  */
 
+#include <string.h>
+
 #include "soup-auth.h"
 #include "soup-error.h"
 #include "soup-message.h"
@@ -623,157 +625,19 @@ soup_message_send (SoupMessage *msg)
 }
 
 static void
-maybe_validate_auth (SoupMessage *msg, gpointer user_data)
-{
-       gboolean proxy = GPOINTER_TO_INT (user_data);
-       int auth_failure;
-       SoupContext *ctx;
-       SoupAuth *auth;
-
-       if (proxy) {
-               ctx = soup_get_proxy ();
-               auth_failure = SOUP_ERROR_PROXY_UNAUTHORIZED; /* 407 */
-       }
-       else {
-               ctx = msg->context;
-               auth_failure = SOUP_ERROR_UNAUTHORIZED; /* 401 */
-       }
-
-       auth = soup_auth_lookup (ctx);
-       if (!auth)
-               return;
-
-       if (msg->errorcode == auth_failure) {
-               /* Pass through */
-       }
-       else if (msg->errorclass == SOUP_ERROR_CLASS_SERVER_ERROR) {
-               /* 
-                * We have no way of knowing whether our auth is any good
-                * anymore, so just invalidate it and start from the
-                * beginning next time.
-                */
-               soup_auth_invalidate (auth, ctx);
-       }
-       else {
-               auth->status = SOUP_AUTH_STATUS_SUCCESSFUL;
-               auth->controlling_msg = NULL;
-       }
-} /* maybe_validate_auth */
-
-static void 
 authorize_handler (SoupMessage *msg, gboolean proxy)
 {
-       const GSList *vals;
-       SoupAuth *auth;
        SoupContext *ctx;
-       const SoupUri *uri;
-
-       if (msg->connection->auth &&
-           msg->connection->auth->status == SOUP_AUTH_STATUS_SUCCESSFUL)
-               goto THROW_CANT_AUTHENTICATE;
 
        ctx = proxy ? soup_get_proxy () : msg->context;
-       uri = soup_context_get_uri (ctx);
-
-       vals = soup_message_get_header_list (msg->response_headers, 
-                                            proxy ? 
-                                                    "Proxy-Authenticate" : 
-                                                    "WWW-Authenticate");
-       if (!vals) goto THROW_CANT_AUTHENTICATE;
-
-       auth = soup_auth_lookup (ctx);
-       if (auth && auth->type == SOUP_AUTH_TYPE_NTLM)
-               auth = NULL;
-
-       if (auth) {
-               g_assert (auth->status != SOUP_AUTH_STATUS_INVALID);
-
-               if (auth->status == SOUP_AUTH_STATUS_PENDING) {
-                       if (auth->controlling_msg == msg) {
-                               auth->status = SOUP_AUTH_STATUS_FAILED;
-                               goto THROW_CANT_AUTHENTICATE;
-                       }
-                       else {
-                               soup_message_requeue (msg);
-                               return;
-                       }
-               }
-               else if (auth->status == SOUP_AUTH_STATUS_FAILED ||
-                        auth->status == SOUP_AUTH_STATUS_SUCCESSFUL) {
-                       /*
-                        * We've failed previously, but we'll give it
-                        * another go, or we've been successful
-                        * previously, but it's not working anymore.
-                        *
-                        * Invalidate the auth, so it's removed from the
-                        * hash and try it again as if we never had it
-                        * in the first place.
-                        */
-                       soup_auth_invalidate (auth, ctx);
-                       soup_message_requeue (msg);
-                       return;
-               }
-       }
-
-       if (!auth) {
-               auth = soup_auth_new_from_header_list (uri, vals);
-
-               if (!auth) {
-                       soup_message_set_error_full (
-                               msg, 
-                               proxy ? 
-                                       SOUP_ERROR_CANT_AUTHENTICATE_PROXY : 
-                                       SOUP_ERROR_CANT_AUTHENTICATE,
-                               proxy ? 
-                                       "Unknown authentication scheme "
-                                       "required by proxy" :
-                                       "Unknown authentication scheme "
-                                       "required");
-                       return;
-               }
-
-               auth->status = SOUP_AUTH_STATUS_PENDING;
-               auth->controlling_msg = msg;
-               soup_message_add_handler (msg, SOUP_HANDLER_PRE_BODY,
-                                         maybe_validate_auth,
-                                         GINT_TO_POINTER (proxy));
-       }
-
-       /*
-        * Call registered authenticate handler
-        */
-       if (!uri->user && soup_auth_fn)
-               (*soup_auth_fn) (auth->type,
-                                (SoupUri *) uri,
-                                auth->realm, 
-                                soup_auth_fn_user_data);
-
-       if (!uri->user) {
-               soup_auth_free (auth);
-               goto THROW_CANT_AUTHENTICATE;
-       }
-
-       /*
-        * Initialize with auth data (possibly returned from 
-        * auth callback).
-        */
-       soup_auth_initialize (auth, uri);
-
-       if (auth->type == SOUP_AUTH_TYPE_NTLM &&
-           auth->status == SOUP_AUTH_STATUS_SUCCESSFUL)
-               msg->connection->auth = auth;
-       else
-               soup_auth_set_context (auth, ctx);
-
-       soup_message_requeue (msg);
-
-        return;
-
- THROW_CANT_AUTHENTICATE:
-       soup_message_set_error (msg, 
-                               proxy ? 
+       if (soup_context_update_auth (ctx, msg))
+               soup_message_requeue (msg);
+       else {
+               soup_message_set_error (msg, 
+                                       proxy ? 
                                        SOUP_ERROR_CANT_AUTHENTICATE_PROXY : 
                                        SOUP_ERROR_CANT_AUTHENTICATE);
+       }
 }
 
 static void 
index 50db591..d67535c 100644 (file)
@@ -46,9 +46,11 @@ extern gpointer        soup_auth_fn_user_data;
 
 typedef struct {
        gchar      *host;
-       GSList     *connections;        /* CONTAINS: SoupConnection */
-       GHashTable *contexts;           /* KEY: uri->path, VALUE: SoupContext */
-       GHashTable *valid_auths;        /* KEY: uri->path, VALUE: SoupAuth */
+       GSList     *connections;      /* CONTAINS: SoupConnection */
+       GHashTable *contexts;         /* KEY: uri->path, VALUE: SoupContext */
+       gboolean    use_ntlm;
+       GHashTable *auth_realms;      /* KEY: uri->path, VALUE: scheme:realm */
+       GHashTable *auths;            /* KEY: scheme:realm, VALUE: SoupAuth */
 } SoupHost;
 
 struct _SoupSocket {
@@ -124,6 +126,20 @@ struct _SoupMessagePrivate {
        SoupServerMessage *server_msg;
 };
 
+/* from soup-context.c */
+
+SoupAuth   *soup_context_lookup_auth       (SoupContext    *ctx,
+                                           SoupMessage    *msg);
+
+gboolean    soup_context_update_auth       (SoupContext    *ctx,
+                                           SoupMessage    *msg);
+
+gboolean    soup_context_authenticate_auth (SoupContext    *ctx,
+                                           SoupAuth       *auth);
+
+void        soup_context_invalidate_auth   (SoupContext    *ctx,
+                                           SoupAuth       *auth);
+                                         
 /* from soup-message.c */
 
 void     soup_message_issue_callback (SoupMessage      *req);
index 7695583..7e5060c 100644 (file)
@@ -295,23 +295,22 @@ soup_encode_http_auth (SoupMessage *msg, GString *header, gboolean proxy_auth)
 
        ctx = proxy_auth ? soup_get_proxy () : msg->context;
 
-       if (msg->connection->auth)
-               auth = msg->connection->auth;
-       else
-               auth = soup_auth_lookup (ctx);
-
-       if (auth) {
-               token = soup_auth_authorize (auth, msg);
-               if (token) {
-                       g_string_sprintfa (header, 
-                                          "%s: %s\r\n",
-                                          proxy_auth ? 
-                                               "Proxy-Authorization" : 
-                                               "Authorization",
-                                          token);
-                       g_free (token);
-               }
-       }
+       auth = soup_context_lookup_auth (ctx, msg);
+       if (!auth)
+               return;
+       if (!auth->authenticated &&
+           !soup_context_authenticate_auth (ctx, auth))
+               return;
+
+       token = soup_auth_authorize (auth, msg);
+       if (token) {
+               g_string_sprintfa (header, "%s: %s\r\n",
+                                  proxy_auth ? 
+                                       "Proxy-Authorization" : 
+                                       "Authorization",
+                                  token);
+               g_free (token);
+       }
 }
 
 struct SoupUsedHeaders {
index ec574b3..c7b7d69 100644 (file)
@@ -2,3 +2,4 @@ Makefile
 Makefile.in
 get
 timeserver
+auth-test
index bd8803b..60bf963 100644 (file)
@@ -2,12 +2,17 @@ INCLUDES =            \
        -I$(top_srcdir) \
        $(GLIB_CFLAGS)
 
-noinst_PROGRAMS = get #timeserver
+noinst_PROGRAMS = get timeserver
 
 get_SOURCES = get.c
 
 get_LDADD = $(top_builddir)/libsoup/libsoup-2.0.la
 
-#timeserver_SOURCES = timeserver.c
+timeserver_SOURCES = timeserver.c
 
-#timeserver_LDADD = $(top_builddir)/libsoup/libsoup-2.0.la
+timeserver_LDADD = $(top_builddir)/libsoup/libsoup-2.0.la
+
+check_PROGRAMS = auth-test
+
+auth_test_SOURCES = auth-test.c
+auth_test_LDADD = $(top_builddir)/libsoup/libsoup-2.0.la
diff --git a/tests/auth-test.c b/tests/auth-test.c
new file mode 100644 (file)
index 0000000..54b5219
--- /dev/null
@@ -0,0 +1,273 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libsoup/soup.h"
+#include "libsoup/soup-auth.h"
+#include "libsoup/soup-private.h"
+
+int errors = 0;
+
+typedef struct {
+       const char *explanation;
+       const char *url;
+       const char *expected;
+       gboolean success;
+} SoupAuthTest;
+
+SoupAuthTest tests[] = {
+       { "No auth available, should fail",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/index.txt",
+         "0", FALSE },
+
+       { "Should fail with no auth, fail again with bad password, and give up",
+         "http://user4:realm4@primates.ximian.com/~danw/soup-test/Basic/realm2/index.txt",
+         "04", FALSE },
+
+       { "Known realm, auth provided, so should succeed immediately",
+         "http://user1:realm1@primates.ximian.com/~danw/soup-test/Basic/realm1/index.txt",
+         "1", TRUE },
+
+       { "Now should automatically reuse previous auth",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/index.txt",
+         "1", TRUE },
+
+       { "Subdir should also automatically reuse auth",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/subdir/index.txt",
+         "1", TRUE },
+
+       { "Subdir should retry last auth, but will fail this time",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/realm2/index.txt",
+         "1", FALSE },
+
+       { "Now should use provided auth on first try",
+         "http://user2:realm2@primates.ximian.com/~danw/soup-test/Basic/realm1/realm2/index.txt",
+         "2", TRUE },
+
+       { "Reusing last auth. Should succeed on first try",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/realm2/index.txt",
+         "2", TRUE },
+
+       { "Reuse will fail, but 2nd try will succeed because it's a known realm",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/realm2/realm1/index.txt",
+         "21", TRUE },
+
+       { "Should succeed on first try. (Known realm with cached password)",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm2/index.txt",
+         "2", TRUE },
+
+       { "Fail once, then use password",
+         "http://user3:realm3@primates.ximian.com/~danw/soup-test/Basic/realm3/index.txt",
+         "03", TRUE },
+
+
+       { "No auth available, should fail",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/index.txt",
+         "0", FALSE },
+
+       { "Should fail with no auth, fail again with bad password, and give up",
+         "http://user4:realm4@primates.ximian.com/~danw/soup-test/Digest/realm2/index.txt",
+         "04", FALSE },
+
+       { "Known realm, auth provided, so should succeed immediately",
+         "http://user1:realm1@primates.ximian.com/~danw/soup-test/Digest/realm1/index.txt",
+         "1", TRUE },
+
+       { "Now should automatically reuse previous auth",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/index.txt",
+         "1", TRUE },
+
+       { "Subdir should also automatically reuse auth",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/subdir/index.txt",
+         "1", TRUE },
+
+       { "Subdir should retry last auth, but will fail this time",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/realm2/index.txt",
+         "1", FALSE },
+
+       { "Now should use provided auth on first try",
+         "http://user2:realm2@primates.ximian.com/~danw/soup-test/Digest/realm1/realm2/index.txt",
+         "2", TRUE },
+
+       { "Reusing last auth. Should succeed on first try",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/realm2/index.txt",
+         "2", TRUE },
+
+       { "Should succeed on first try because of earlier domain directive",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/realm2/realm1/index.txt",
+         "1", TRUE },
+
+       { "Should succeed on first try. (Known realm with cached password)",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm2/index.txt",
+         "2", TRUE },
+
+       { "Fail once, then use password",
+         "http://user3:realm3@primates.ximian.com/~danw/soup-test/Digest/realm3/index.txt",
+         "03", TRUE },
+
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/index.txt",
+         "1", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/realm2/index.txt",
+         "2", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm1/realm2/realm1/index.txt",
+         "1", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm2/index.txt",
+         "2", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Basic/realm3/index.txt",
+         "3", TRUE },
+
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/index.txt",
+         "1", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/realm2/index.txt",
+         "2", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm1/realm2/realm1/index.txt",
+         "1", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm2/index.txt",
+         "2", TRUE },
+
+       { "Make sure we haven't forgotten anything",
+         "http://primates.ximian.com/~danw/soup-test/Digest/realm3/index.txt",
+         "3", TRUE }
+};
+int ntests = sizeof (tests) / sizeof (tests[0]);
+
+static const char *auths[] = {
+       "no password", "password 1",
+       "password 2", "password 3",
+       "intentionally wrong password",
+};
+
+static int
+identify_auth (SoupMessage *msg)
+{
+       SoupAuth *auth;
+       char *header;
+       int num;
+
+       auth = soup_context_lookup_auth (msg->context, msg);
+       if (!auth || !auth->authenticated)
+               return 0;
+
+       header = soup_auth_authorize (auth, msg);
+       if (!g_ascii_strncasecmp (header, "Basic ", 6)) {
+               char *token;
+               int len;
+
+               token = soup_base64_decode (header + 6, &len);
+               num = token[len - 1] - '0';
+               g_free (token);
+       } else {
+               const char *user;
+
+               user = strstr (header, "username=\"user");
+               if (user)
+                       num = user[14] - '0';
+               else
+                       num = 0;
+       }
+
+       g_free (header);
+       return num;
+}
+
+static void
+handler (SoupMessage *msg, gpointer data)
+{
+       char *expected = data;
+       int auth, exp;
+
+       auth = identify_auth (msg);
+
+       printf ("  %d %s (using %s)\n", msg->errorcode, msg->errorphrase,
+               auths[auth]);
+
+       if (*expected) {
+               exp = *expected - '0';
+               if (auth != exp) {
+                       printf ("    expected %s!\n", auths[exp]);
+                       errors++;
+               }
+               memmove (expected, expected + 1, strlen (expected));
+       } else {
+               printf ("    expected to be finished\n");
+               errors++;
+       }
+}
+
+int
+main (int argc, char **argv)
+{
+        SoupContext *ctx;
+        SoupMessage *msg;
+       char *expected;
+       int i;
+
+       for (i = 0; i < ntests; i++) {
+               printf ("Test %d: %s\n", i + 1, tests[i].explanation);
+
+               printf ("  GET %s\n", tests[i].url);
+               ctx = soup_context_get (tests[i].url);
+               if (!ctx) {
+                       fprintf (stderr, "auth-test: Could not parse URI\n");
+                       exit (1);
+               }
+
+               msg = soup_message_new (ctx, SOUP_METHOD_GET);
+
+               expected = g_strdup (tests[i].expected);
+               soup_message_add_error_code_handler (
+                       msg, SOUP_ERROR_UNAUTHORIZED,
+                       SOUP_HANDLER_PRE_BODY, handler, expected);
+               soup_message_add_error_code_handler (
+                       msg, SOUP_ERROR_OK, SOUP_HANDLER_PRE_BODY,
+                       handler, expected);
+               soup_message_send (msg);
+               if (msg->errorcode != SOUP_ERROR_CANT_AUTHENTICATE &&
+                   msg->errorcode != SOUP_ERROR_OK) {
+                       printf ("  %d %s !\n", msg->errorcode,
+                               msg->errorphrase);
+               }
+               if (*expected) {
+                       printf ("  expected %d more round(s)\n",
+                               strlen (expected));
+                       errors++;
+               }
+               g_free (expected);
+
+               if (SOUP_ERROR_IS_SUCCESSFUL (msg->errorcode) !=
+                   tests[i].success) {
+                       printf ("  expected %s\n",
+                               tests[i].success ? "success" : "failure");
+                       errors++;
+               }
+
+               printf ("\n");
+
+               soup_message_free (msg);
+               /* We don't free ctx because if we did, we'd end up
+                * discarding the cached auth info at the end of each
+                * test.
+                */
+       }
+
+       printf ("\nauth-test: %d errors\n", errors);
+       return errors;
+}