11 #include "libsoup/soup.h"
12 #include "libsoup/soup-auth.h"
13 #include "libsoup/soup-session.h"
15 #include "test-utils.h"
20 /* Explanation of what you should see */
21 const char *explanation;
23 /* URL to test against */
26 /* Provided passwords, 1 character each. ('1', '2', and '3'
27 * mean the correct passwords for "realm1", "realm2", and
28 * "realm3" respectively. '4' means "use the wrong password".)
29 * The first password (if present) will be used by
30 * authenticate(), and the second (if present) will be used by
35 /* Expected passwords, 1 character each. (As with the provided
36 * passwords, with the addition that '0' means "no
37 * Authorization header expected".) Used to verify that soup
38 * used the password it was supposed to at each step.
42 /* What the final status code should be. */
46 SoupAuthTest tests[] = {
47 { "No auth available, should fail",
48 "Basic/realm1/", "", "0", SOUP_STATUS_UNAUTHORIZED },
50 { "Should fail with no auth, fail again with bad password, and give up",
51 "Basic/realm2/", "4", "04", SOUP_STATUS_UNAUTHORIZED },
53 { "Known realm, auth provided, so should succeed immediately",
54 "Basic/realm1/", "1", "1", SOUP_STATUS_OK },
56 { "Now should automatically reuse previous auth",
57 "Basic/realm1/", "", "1", SOUP_STATUS_OK },
59 { "Subdir should also automatically reuse auth",
60 "Basic/realm1/subdir/", "", "1", SOUP_STATUS_OK },
62 { "Subdir should retry last auth, but will fail this time",
63 "Basic/realm1/realm2/", "", "1", SOUP_STATUS_UNAUTHORIZED },
65 { "Now should use provided auth on first try",
66 "Basic/realm1/realm2/", "2", "2", SOUP_STATUS_OK },
68 { "Reusing last auth. Should succeed on first try",
69 "Basic/realm1/realm2/", "", "2", SOUP_STATUS_OK },
71 { "Reuse will fail, but 2nd try will succeed because it's a known realm",
72 "Basic/realm1/realm2/realm1/", "", "21", SOUP_STATUS_OK },
74 { "Should succeed on first try. (Known realm with cached password)",
75 "Basic/realm2/", "", "2", SOUP_STATUS_OK },
77 { "Fail once, then use typoed password, then use right password",
78 "Basic/realm3/", "43", "043", SOUP_STATUS_OK },
81 { "No auth available, should fail",
82 "Digest/realm1/", "", "0", SOUP_STATUS_UNAUTHORIZED },
84 { "Should fail with no auth, fail again with bad password, and give up",
85 "Digest/realm2/", "4", "04", SOUP_STATUS_UNAUTHORIZED },
87 { "Known realm, auth provided, so should succeed immediately",
88 "Digest/realm1/", "1", "1", SOUP_STATUS_OK },
90 { "Now should automatically reuse previous auth",
91 "Digest/realm1/", "", "1", SOUP_STATUS_OK },
93 { "Subdir should also automatically reuse auth",
94 "Digest/realm1/subdir/", "", "1", SOUP_STATUS_OK },
96 { "Should already know correct domain and use provided auth on first try",
97 "Digest/realm1/realm2/", "2", "2", SOUP_STATUS_OK },
99 { "Reusing last auth. Should succeed on first try",
100 "Digest/realm1/realm2/", "", "2", SOUP_STATUS_OK },
102 { "Should succeed on first try because of earlier domain directive",
103 "Digest/realm1/realm2/realm1/", "", "1", SOUP_STATUS_OK },
105 { "Should succeed on first try. (Known realm with cached password)",
106 "Digest/realm2/", "", "2", SOUP_STATUS_OK },
108 { "Fail once, then use typoed password, then use right password",
109 "Digest/realm3/", "43", "043", SOUP_STATUS_OK },
112 { "Make sure we haven't forgotten anything",
113 "Basic/realm1/", "", "1", SOUP_STATUS_OK },
115 { "Make sure we haven't forgotten anything",
116 "Basic/realm1/realm2/", "", "2", SOUP_STATUS_OK },
118 { "Make sure we haven't forgotten anything",
119 "Basic/realm1/realm2/realm1/", "", "1", SOUP_STATUS_OK },
121 { "Make sure we haven't forgotten anything",
122 "Basic/realm2/", "", "2", SOUP_STATUS_OK },
124 { "Make sure we haven't forgotten anything",
125 "Basic/realm3/", "", "3", SOUP_STATUS_OK },
128 { "Make sure we haven't forgotten anything",
129 "Digest/realm1/", "", "1", SOUP_STATUS_OK },
131 { "Make sure we haven't forgotten anything",
132 "Digest/realm1/realm2/", "", "2", SOUP_STATUS_OK },
134 { "Make sure we haven't forgotten anything",
135 "Digest/realm1/realm2/realm1/", "", "1", SOUP_STATUS_OK },
137 { "Make sure we haven't forgotten anything",
138 "Digest/realm2/", "", "2", SOUP_STATUS_OK },
140 { "Make sure we haven't forgotten anything",
141 "Digest/realm3/", "", "3", SOUP_STATUS_OK },
143 { "Now the server will reject the formerly-good password",
144 "Basic/realm1/not/", "1" /* should not be used */, "1", SOUP_STATUS_UNAUTHORIZED },
146 { "Make sure we've forgotten it",
147 "Basic/realm1/", "", "0", SOUP_STATUS_UNAUTHORIZED },
149 { "Likewise, reject the formerly-good Digest password",
150 "Digest/realm1/not/", "1" /* should not be used */, "1", SOUP_STATUS_UNAUTHORIZED },
152 { "Make sure we've forgotten it",
153 "Digest/realm1/", "", "0", SOUP_STATUS_UNAUTHORIZED }
155 int ntests = sizeof (tests) / sizeof (tests[0]);
157 static const char *auths[] = {
158 "no password", "password 1",
159 "password 2", "password 3",
160 "intentionally wrong password",
164 identify_auth (SoupMessage *msg)
169 header = soup_message_headers_get (msg->request_headers,
174 if (!g_ascii_strncasecmp (header, "Basic ", 6)) {
178 token = (char *)g_base64_decode (header + 6, &len);
179 num = token[len - 1] - '0';
184 user = strstr (header, "username=\"user");
186 num = user[14] - '0';
191 g_assert (num >= 0 && num <= 4);
197 handler (SoupMessage *msg, gpointer data)
199 char *expected = data;
202 auth = identify_auth (msg);
204 debug_printf (1, " %d %s (using %s)\n",
205 msg->status_code, msg->reason_phrase,
209 exp = *expected - '0';
211 debug_printf (1, " expected %s!\n", auths[exp]);
214 memmove (expected, expected + 1, strlen (expected));
216 debug_printf (1, " expected to be finished\n");
222 authenticate (SoupSession *session, SoupMessage *msg,
223 SoupAuth *auth, gboolean retrying, gpointer data)
226 char *username, *password;
229 if (!tests[*i].provided[0])
232 if (!tests[*i].provided[1])
234 num = tests[*i].provided[1];
236 num = tests[*i].provided[0];
238 username = g_strdup_printf ("user%c", num);
239 password = g_strdup_printf ("realm%c", num);
240 soup_auth_authenticate (auth, username, password);
246 bug271540_sent (SoupMessage *msg, gpointer data)
248 int n = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "#"));
249 gboolean *authenticated = data;
250 int auth = identify_auth (msg);
252 if (!*authenticated && auth) {
253 debug_printf (1, " using auth on message %d before authenticating!!??\n", n);
255 } else if (*authenticated && !auth) {
256 debug_printf (1, " sent unauthenticated message %d after authenticating!\n", n);
262 bug271540_authenticate (SoupSession *session, SoupMessage *msg,
263 SoupAuth *auth, gboolean retrying, gpointer data)
265 int n = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "#"));
266 gboolean *authenticated = data;
268 if (strcmp (soup_auth_get_scheme_name (auth), "Basic") != 0 ||
269 strcmp (soup_auth_get_realm (auth), "realm1") != 0)
272 if (!*authenticated) {
273 debug_printf (1, " authenticating message %d\n", n);
274 soup_auth_authenticate (auth, "user1", "realm1");
275 *authenticated = TRUE;
277 debug_printf (1, " asked to authenticate message %d after authenticating!\n", n);
283 bug271540_finished (SoupSession *session, SoupMessage *msg, gpointer data)
286 int n = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "#"));
288 if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
289 debug_printf (1, " got status '%d %s' on message %d!\n",
290 msg->status_code, msg->reason_phrase, n);
296 g_main_loop_quit (loop);
300 digest_nonce_authenticate (SoupSession *session, SoupMessage *msg,
301 SoupAuth *auth, gboolean retrying, gpointer data)
306 if (strcmp (soup_auth_get_scheme_name (auth), "Digest") != 0 ||
307 strcmp (soup_auth_get_realm (auth), "realm1") != 0)
310 soup_auth_authenticate (auth, "user1", "realm1");
314 digest_nonce_unauthorized (SoupMessage *msg, gpointer data)
316 gboolean *got_401 = data;
321 do_digest_nonce_test (SoupSession *session,
322 const char *nth, const char *uri,
323 gboolean expect_401, gboolean expect_signal)
328 msg = soup_message_new (SOUP_METHOD_GET, uri);
330 g_signal_connect (session, "authenticate",
331 G_CALLBACK (digest_nonce_authenticate),
334 soup_message_add_status_code_handler (msg, "got_headers",
335 SOUP_STATUS_UNAUTHORIZED,
336 G_CALLBACK (digest_nonce_unauthorized),
339 soup_session_send_message (session, msg);
340 if (got_401 != expect_401) {
341 debug_printf (1, " %s request %s a 401 Unauthorized!\n", nth,
342 got_401 ? "got" : "did not get");
345 if (msg->status_code != SOUP_STATUS_OK) {
346 debug_printf (1, " %s request got status %d %s!\n", nth,
347 msg->status_code, msg->reason_phrase);
351 debug_printf (1, " %s request succeeded\n", nth);
352 g_object_unref (msg);
355 /* Async auth test. We queue three requests to /Basic/realm1, ensuring
356 * that they are sent in order. The first and third ones will be
357 * paused from the authentication callback. The second will be allowed
358 * to fail. Shortly after the third one requests auth, we'll provide
359 * the auth and unpause the two remaining messages, allowing them to
364 async_authenticate (SoupSession *session, SoupMessage *msg,
365 SoupAuth *auth, gboolean retrying, gpointer data)
367 SoupAuth **saved_auth = data;
368 int id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "id"));
370 debug_printf (2, " async_authenticate msg%d\n", id);
372 /* The session will try to authenticate msg3 *before* sending
373 * it, because it already knows it's going to need the auth.
376 if (msg->status_code != SOUP_STATUS_UNAUTHORIZED) {
377 debug_printf (2, " (ignoring)\n");
381 soup_session_pause_message (session, msg);
383 *saved_auth = g_object_ref (auth);
384 g_main_loop_quit (loop);
388 async_finished (SoupSession *session, SoupMessage *msg, gpointer user_data)
390 int *finished = user_data;
391 int id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "id"));
393 debug_printf (2, " async_finished msg%d\n", id);
397 g_main_loop_quit (loop);
401 async_authenticate_522601 (SoupSession *session, SoupMessage *msg,
402 SoupAuth *auth, gboolean retrying, gpointer data)
404 gboolean *been_here = data;
406 debug_printf (2, " async_authenticate_522601\n");
409 debug_printf (1, " ERROR: async_authenticate_522601 called twice\n");
414 soup_session_pause_message (session, msg);
415 g_main_loop_quit (loop);
419 do_async_auth_test (const char *base_uri)
421 SoupSession *session;
422 SoupMessage *msg1, *msg2, *msg3, msg2_bak;
425 SoupAuth *auth = NULL;
427 gboolean been_there = FALSE;
429 debug_printf (1, "\nTesting async auth:\n");
431 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
433 uri = g_strconcat (base_uri, "Basic/realm1/", NULL);
435 msg1 = soup_message_new ("GET", uri);
436 g_object_set_data (G_OBJECT (msg1), "id", GINT_TO_POINTER (1));
437 auth_id = g_signal_connect (session, "authenticate",
438 G_CALLBACK (async_authenticate), &auth);
440 soup_session_queue_message (session, msg1, async_finished, &finished);
441 g_main_loop_run (loop);
442 g_signal_handler_disconnect (session, auth_id);
444 /* async_authenticate will pause msg1 and quit loop */
446 msg2 = soup_message_new ("GET", uri);
447 g_object_set_data (G_OBJECT (msg2), "id", GINT_TO_POINTER (2));
448 soup_session_send_message (session, msg2);
450 if (msg2->status_code == SOUP_STATUS_UNAUTHORIZED)
451 debug_printf (1, " msg2 failed as expected\n");
453 debug_printf (1, " msg2 got wrong status! (%u)\n",
458 /* msg2 should be done at this point; assuming everything is
459 * working correctly, the session won't look at it again; we
460 * ensure that if it does, it will crash the test program.
462 memcpy (&msg2_bak, msg2, sizeof (SoupMessage));
463 memset (msg2, 0, sizeof (SoupMessage));
465 msg3 = soup_message_new ("GET", uri);
466 g_object_set_data (G_OBJECT (msg3), "id", GINT_TO_POINTER (3));
467 auth_id = g_signal_connect (session, "authenticate",
468 G_CALLBACK (async_authenticate), NULL);
470 soup_session_queue_message (session, msg3, async_finished, &finished);
471 g_main_loop_run (loop);
472 g_signal_handler_disconnect (session, auth_id);
474 /* async_authenticate will pause msg3 and quit loop */
476 /* Now do the auth, and restart */
478 soup_auth_authenticate (auth, "user1", "realm1");
479 g_object_unref (auth);
480 soup_session_unpause_message (session, msg1);
481 soup_session_unpause_message (session, msg3);
483 g_main_loop_run (loop);
485 /* async_finished will quit the loop */
487 debug_printf (1, " msg1 didn't get authenticate signal!\n");
491 if (msg1->status_code == SOUP_STATUS_OK)
492 debug_printf (1, " msg1 succeeded\n");
494 debug_printf (1, " msg1 FAILED! (%u %s)\n",
495 msg1->status_code, msg1->reason_phrase);
498 if (msg3->status_code == SOUP_STATUS_OK)
499 debug_printf (1, " msg3 succeeded\n");
501 debug_printf (1, " msg3 FAILED! (%u %s)\n",
502 msg3->status_code, msg3->reason_phrase);
506 soup_session_abort (session);
507 g_object_unref (session);
509 g_object_unref (msg1);
510 g_object_unref (msg3);
511 memcpy (msg2, &msg2_bak, sizeof (SoupMessage));
512 g_object_unref (msg2);
514 /* Test that giving the wrong password doesn't cause multiple
515 * authenticate signals the second time.
517 debug_printf (1, "\nTesting async auth with wrong password (#522601):\n");
519 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
522 msg1 = soup_message_new ("GET", uri);
523 g_object_set_data (G_OBJECT (msg1), "id", GINT_TO_POINTER (1));
524 auth_id = g_signal_connect (session, "authenticate",
525 G_CALLBACK (async_authenticate), &auth);
527 soup_session_queue_message (session, msg1, async_finished, &finished);
528 g_main_loop_run (loop);
529 g_signal_handler_disconnect (session, auth_id);
530 soup_auth_authenticate (auth, "user1", "wrong");
531 g_object_unref (auth);
532 soup_session_unpause_message (session, msg1);
534 auth_id = g_signal_connect (session, "authenticate",
535 G_CALLBACK (async_authenticate_522601),
537 g_main_loop_run (loop);
538 g_signal_handler_disconnect (session, auth_id);
540 soup_session_abort (session);
541 g_object_unref (session);
543 g_object_unref (msg1);
549 main (int argc, char **argv)
551 SoupSession *session;
553 char *base_uri, *uri, *expected;
554 gboolean authenticated;
557 test_init (argc, argv, NULL);
560 base_uri = "http://localhost:47524/";
562 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
563 g_signal_connect (session, "authenticate",
564 G_CALLBACK (authenticate), &i);
566 for (i = 0; i < ntests; i++) {
567 debug_printf (1, "Test %d: %s\n", i + 1, tests[i].explanation);
569 uri = g_strconcat (base_uri, tests[i].url, NULL);
570 debug_printf (1, " GET %s\n", uri);
572 msg = soup_message_new (SOUP_METHOD_GET, uri);
575 fprintf (stderr, "auth-test: Could not parse URI\n");
579 expected = g_strdup (tests[i].expected);
580 soup_message_add_status_code_handler (
581 msg, "got_headers", SOUP_STATUS_UNAUTHORIZED,
582 G_CALLBACK (handler), expected);
583 soup_message_add_status_code_handler (
584 msg, "got_headers", SOUP_STATUS_OK,
585 G_CALLBACK (handler), expected);
586 soup_session_send_message (session, msg);
587 if (msg->status_code != SOUP_STATUS_UNAUTHORIZED &&
588 msg->status_code != SOUP_STATUS_OK) {
589 debug_printf (1, " %d %s !\n", msg->status_code,
594 debug_printf (1, " expected %d more round(s)\n",
595 (int)strlen (expected));
600 if (msg->status_code != tests[i].final_status) {
601 debug_printf (1, " expected %d\n",
602 tests[i].final_status);
605 debug_printf (1, "\n");
607 g_object_unref (msg);
609 soup_session_abort (session);
610 g_object_unref (session);
612 /* And now for some regression tests */
613 loop = g_main_loop_new (NULL, TRUE);
615 debug_printf (1, "Testing pipelined auth (bug 271540):\n");
616 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
618 authenticated = FALSE;
619 g_signal_connect (session, "authenticate",
620 G_CALLBACK (bug271540_authenticate), &authenticated);
622 uri = g_strconcat (base_uri, "Basic/realm1/", NULL);
623 for (i = 0; i < 10; i++) {
624 msg = soup_message_new (SOUP_METHOD_GET, uri);
625 g_object_set_data (G_OBJECT (msg), "#", GINT_TO_POINTER (i + 1));
626 g_signal_connect (msg, "wrote_headers",
627 G_CALLBACK (bug271540_sent), &authenticated);
629 soup_session_queue_message (session, msg,
630 bug271540_finished, &i);
634 g_main_loop_run (loop);
635 soup_session_abort (session);
636 g_object_unref (session);
638 debug_printf (1, "\nTesting digest nonce expiration:\n");
640 /* We test two different things here:
642 * 1. If we get a 401 response with
643 * "WWW-Authenticate: Digest stale=true...", we should
644 * retry and succeed *without* the session asking for a
647 * 2. If we get a successful response with
648 * "Authentication-Info: nextnonce=...", we should update
649 * the nonce automatically so as to avoid getting a
650 * stale nonce error on the next request.
652 * In our Apache config, /Digest/realm1 and
653 * /Digest/realm1/expire are set up to use the same auth info,
654 * but only the latter has an AuthDigestNonceLifetime (of 2
655 * seconds). The way nonces work in Apache, a nonce received
656 * from /Digest/realm1 will still expire in
657 * /Digest/realm1/expire, but it won't issue a nextnonce for a
658 * request in /Digest/realm1. This lets us test both
661 * The expected conversation is:
667 * WWW-Authenticate: Digest nonce=A
669 * [emit 'authenticate']
672 * Authorization: Digest nonce=A
675 * [No Authentication-Info]
677 * [sleep 2 seconds: nonce A is no longer valid, but we have no
678 * way of knowing that]
681 * GET /Digest/realm1/expire/
682 * Authorization: Digest nonce=A
685 * WWW-Authenticate: Digest stale=true nonce=B
687 * GET /Digest/realm1/expire/
688 * Authorization: Digest nonce=B
691 * Authentication-Info: nextnonce=C
696 * GET /Digest/realm1/expire/
697 * Authorization: Digest nonce=C
698 * [nonce=B would work here too]
701 * Authentication-Info: nextnonce=D
703 * [sleep 1 second; nonces B and C are no longer valid, but D is]
706 * GET /Digest/realm1/expire/
707 * Authorization: Digest nonce=D
710 * Authentication-Info: nextnonce=D
714 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
716 uri = g_strconcat (base_uri, "Digest/realm1/", NULL);
717 do_digest_nonce_test (session, "First", uri, TRUE, TRUE);
720 uri = g_strconcat (base_uri, "Digest/realm1/expire/", NULL);
721 do_digest_nonce_test (session, "Second", uri, TRUE, FALSE);
723 do_digest_nonce_test (session, "Third", uri, FALSE, FALSE);
725 do_digest_nonce_test (session, "Fourth", uri, FALSE, FALSE);
728 soup_session_abort (session);
729 g_object_unref (session);
732 do_async_auth_test (base_uri);
734 g_main_loop_unref (loop);