1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * soup-auth-manager.c: SoupAuth manager for SoupSession
5 * Copyright (C) 2007 Red Hat, Inc.
14 #include "soup-auth-manager.h"
16 #include "soup-connection-auth.h"
17 #include "soup-marshal.h"
18 #include "soup-message-private.h"
19 #include "soup-message-queue.h"
20 #include "soup-path-map.h"
21 #include "soup-session-private.h"
24 * SECTION:soup-auth-manager
25 * @short_description: HTTP client-side authentication handler
26 * @see_also: #SoupSession, #SoupAuth
28 * #SoupAuthManager is the #SoupSessionFeature that handles HTTP
29 * authentication for a #SoupSession.
31 * A #SoupAuthManager is added to the session by default, and normally
32 * you don't need to worry about it at all. However, if you want to
33 * disable HTTP authentication, you can remove the feature from the
34 * session with soup_session_remove_feature_by_type(), or disable it on
35 * individual requests with soup_message_disable_feature().
41 * SOUP_TYPE_AUTH_MANAGER:
43 * The #GType of #SoupAuthManager; you can use this with
44 * soup_session_remove_feature_by_type() or
45 * soup_message_disable_feature().
47 * (Although this type has only been publicly visible since libsoup
48 * 2.42, it has always existed in the background, and you can use
49 * <literal><code>g_type_from_name ("SoupAuthManager")</code></literal>
50 * to get its #GType in earlier releases.)
54 static void soup_auth_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
55 static SoupSessionFeatureInterface *soup_session_feature_default_interface;
62 static guint signals[LAST_SIGNAL] = { 0 };
64 G_DEFINE_TYPE_WITH_CODE (SoupAuthManager, soup_auth_manager, G_TYPE_OBJECT,
65 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
66 soup_auth_manager_session_feature_init))
68 struct SoupAuthManagerPrivate {
70 GPtrArray *auth_types;
75 GHashTable *auth_hosts;
80 SoupPathMap *auth_realms; /* path -> scheme:realm */
81 GHashTable *auths; /* scheme:realm -> SoupAuth */
84 static void soup_auth_host_free (SoupAuthHost *host);
85 static SoupAuth *record_auth_for_uri (SoupAuthManagerPrivate *priv,
86 SoupURI *uri, SoupAuth *auth,
87 gboolean prior_auth_failed);
90 soup_auth_manager_init (SoupAuthManager *manager)
92 SoupAuthManagerPrivate *priv;
94 priv = manager->priv = G_TYPE_INSTANCE_GET_PRIVATE (manager, SOUP_TYPE_AUTH_MANAGER, SoupAuthManagerPrivate);
96 priv->auth_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref);
97 priv->auth_hosts = g_hash_table_new_full (soup_uri_host_hash,
100 (GDestroyNotify)soup_auth_host_free);
101 g_mutex_init (&priv->lock);
105 soup_auth_manager_finalize (GObject *object)
107 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (object)->priv;
109 g_ptr_array_free (priv->auth_types, TRUE);
111 g_hash_table_destroy (priv->auth_hosts);
113 g_clear_object (&priv->proxy_auth);
115 g_mutex_clear (&priv->lock);
117 G_OBJECT_CLASS (soup_auth_manager_parent_class)->finalize (object);
121 soup_auth_manager_class_init (SoupAuthManagerClass *auth_manager_class)
123 GObjectClass *object_class = G_OBJECT_CLASS (auth_manager_class);
125 g_type_class_add_private (auth_manager_class, sizeof (SoupAuthManagerPrivate));
127 object_class->finalize = soup_auth_manager_finalize;
130 * SoupAuthManager::authenticate:
131 * @manager: the #SoupAuthManager
132 * @msg: the #SoupMessage being sent
133 * @auth: the #SoupAuth to authenticate
134 * @retrying: %TRUE if this is the second (or later) attempt
136 * Emitted when the manager requires the application to
137 * provide authentication credentials.
139 * #SoupSession connects to this signal and emits its own
140 * #SoupSession::authenticate signal when it is emitted, so
141 * you shouldn't need to use this signal directly.
143 signals[AUTHENTICATE] =
144 g_signal_new ("authenticate",
145 G_OBJECT_CLASS_TYPE (object_class),
147 G_STRUCT_OFFSET (SoupAuthManagerClass, authenticate),
149 _soup_marshal_NONE__OBJECT_OBJECT_BOOLEAN,
158 auth_type_compare_func (gconstpointer a, gconstpointer b)
160 SoupAuthClass **auth1 = (SoupAuthClass **)a;
161 SoupAuthClass **auth2 = (SoupAuthClass **)b;
163 return (*auth1)->strength - (*auth2)->strength;
167 soup_auth_manager_add_feature (SoupSessionFeature *feature, GType type)
169 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
170 SoupAuthClass *auth_class;
172 if (!g_type_is_a (type, SOUP_TYPE_AUTH))
175 auth_class = g_type_class_ref (type);
176 g_ptr_array_add (priv->auth_types, auth_class);
177 g_ptr_array_sort (priv->auth_types, auth_type_compare_func);
179 /* Plain SoupSession does not get the backward-compat
180 * auto-NTLM behavior; SoupSession subclasses do.
182 if (type == SOUP_TYPE_AUTH_NTLM &&
183 G_TYPE_FROM_INSTANCE (priv->session) != SOUP_TYPE_SESSION)
184 priv->auto_ntlm = TRUE;
190 soup_auth_manager_remove_feature (SoupSessionFeature *feature, GType type)
192 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
193 SoupAuthClass *auth_class;
196 if (!g_type_is_a (type, SOUP_TYPE_AUTH))
199 auth_class = g_type_class_peek (type);
201 for (i = 0; i < priv->auth_types->len; i++) {
202 if (priv->auth_types->pdata[i] == (gpointer)auth_class) {
203 if (type == SOUP_TYPE_AUTH_NTLM)
204 priv->auto_ntlm = FALSE;
206 g_ptr_array_remove_index (priv->auth_types, i);
215 soup_auth_manager_has_feature (SoupSessionFeature *feature, GType type)
217 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
218 SoupAuthClass *auth_class;
221 if (!g_type_is_a (type, SOUP_TYPE_AUTH))
224 auth_class = g_type_class_peek (type);
225 for (i = 0; i < priv->auth_types->len; i++) {
226 if (priv->auth_types->pdata[i] == (gpointer)auth_class)
233 soup_auth_manager_attach (SoupSessionFeature *feature, SoupSession *session)
235 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
237 /* FIXME: should support multiple sessions */
238 priv->session = session;
240 soup_session_feature_default_interface->attach (feature, session);
243 static inline const char *
244 auth_header_for_message (SoupMessage *msg)
246 if (msg->status_code == SOUP_STATUS_PROXY_UNAUTHORIZED) {
247 return soup_message_headers_get_list (msg->response_headers,
248 "Proxy-Authenticate");
250 return soup_message_headers_get_list (msg->response_headers,
256 next_challenge_start (GSList *items)
258 /* The relevant grammar (from httpbis):
260 * WWW-Authenticate = 1#challenge
261 * Proxy-Authenticate = 1#challenge
262 * challenge = auth-scheme [ 1*SP ( b64token / #auth-param ) ]
263 * auth-scheme = token
264 * auth-param = token BWS "=" BWS ( token / quoted-string )
265 * b64token = 1*( ALPHA / DIGIT /
266 * "-" / "." / "_" / "~" / "+" / "/" ) *"="
268 * The fact that quoted-strings can contain commas, equals
269 * signs, and auth scheme names makes it tricky to "cheat" on
270 * the parsing. So soup_auth_manager_extract_challenge() will
271 * have used soup_header_parse_list() to split the header into
272 * items. Given the grammar above, the possible items are:
275 * auth-scheme 1*SP b64token
276 * auth-scheme 1*SP auth-param
279 * where the first three represent the start of a new challenge and
280 * the last one does not.
283 for (; items; items = items->next) {
284 const char *item = items->data;
285 const char *sp = strpbrk (item, "\t\r\n ");
286 const char *eq = strchr (item, '=');
289 /* No "=", so it can't be an auth-param */
292 if (!sp || sp > eq) {
293 /* No space, or first space appears after the "=",
294 * so it must be an auth-param.
298 while (g_ascii_isspace (*++sp))
301 /* First "=" appears immediately after the first
302 * space, so this must be an auth-param with
303 * space around the "=".
308 /* "auth-scheme auth-param" or "auth-scheme b64token" */
316 soup_auth_manager_extract_challenge (const char *challenges, const char *scheme)
318 GSList *items, *i, *next;
319 int schemelen = strlen (scheme);
323 items = soup_header_parse_list (challenges);
325 /* First item will start with the scheme name, followed by
326 * either nothing, or else a space and then the first
329 for (i = items; i; i = next_challenge_start (i->next)) {
331 if (!g_ascii_strncasecmp (item, scheme, schemelen) &&
332 (!item[schemelen] || g_ascii_isspace (item[schemelen])))
336 soup_header_free_list (items);
340 next = next_challenge_start (i->next);
341 challenge = g_string_new (item);
342 for (i = i->next; i != next; i = i->next) {
344 g_string_append (challenge, ", ");
345 g_string_append (challenge, item);
348 soup_header_free_list (items);
349 return g_string_free (challenge, FALSE);
353 create_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
356 SoupAuthClass *auth_class;
357 char *challenge = NULL;
361 header = auth_header_for_message (msg);
365 for (i = priv->auth_types->len - 1; i >= 0; i--) {
366 auth_class = priv->auth_types->pdata[i];
367 challenge = soup_auth_manager_extract_challenge (header, auth_class->scheme_name);
374 auth = soup_auth_new (G_TYPE_FROM_CLASS (auth_class), msg, challenge);
380 check_auth (SoupMessage *msg, SoupAuth *auth)
382 const char *header, *scheme;
383 char *challenge = NULL;
386 scheme = soup_auth_get_scheme_name (auth);
388 header = auth_header_for_message (msg);
390 challenge = soup_auth_manager_extract_challenge (header, scheme);
393 challenge = g_strdup (scheme);
396 if (!soup_auth_update (auth, msg, challenge))
402 static SoupAuthHost *
403 get_auth_host_for_uri (SoupAuthManagerPrivate *priv, SoupURI *uri)
407 host = g_hash_table_lookup (priv->auth_hosts, uri);
411 host = g_slice_new0 (SoupAuthHost);
412 host->uri = soup_uri_copy_host (uri);
413 g_hash_table_insert (priv->auth_hosts, host->uri, host);
419 soup_auth_host_free (SoupAuthHost *host)
421 g_clear_pointer (&host->auth_realms, soup_path_map_free);
422 g_clear_pointer (&host->auths, g_hash_table_destroy);
424 soup_uri_free (host->uri);
425 g_slice_free (SoupAuthHost, host);
429 make_auto_ntlm_auth (SoupAuthManagerPrivate *priv, SoupAuthHost *host)
433 if (!priv->auto_ntlm)
436 auth = g_object_new (SOUP_TYPE_AUTH_NTLM,
437 SOUP_AUTH_HOST, host->uri->host,
439 record_auth_for_uri (priv, host->uri, auth, FALSE);
440 g_object_unref (auth);
445 lookup_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
448 const char *path, *realm;
450 host = get_auth_host_for_uri (priv, soup_message_get_uri (msg));
451 if (!host->auth_realms && !make_auto_ntlm_auth (priv, host))
454 path = soup_message_get_uri (msg)->path;
457 realm = soup_path_map_lookup (host->auth_realms, path);
459 return g_hash_table_lookup (host->auths, realm);
465 authenticate_auth (SoupAuthManager *manager, SoupAuth *auth,
466 SoupMessage *msg, gboolean prior_auth_failed,
467 gboolean proxy, gboolean can_interact)
469 SoupAuthManagerPrivate *priv = manager->priv;
473 SoupMessageQueue *queue;
474 SoupMessageQueueItem *item;
476 queue = soup_session_get_queue (priv->session);
477 item = soup_message_queue_lookup (queue, msg);
479 uri = soup_connection_get_proxy_uri (item->conn);
480 soup_message_queue_item_unref (item);
487 uri = soup_message_get_uri (msg);
489 /* If a password is specified explicitly in the URI, use it
490 * even if the auth had previously already been authenticated.
492 if (uri->password && uri->user) {
493 soup_auth_authenticate (auth, uri->user, uri->password);
494 soup_uri_set_password (uri, NULL);
495 soup_uri_set_user (uri, NULL);
496 } else if (!soup_auth_is_authenticated (auth) && can_interact) {
497 g_signal_emit (manager, signals[AUTHENTICATE], 0,
498 msg, auth, prior_auth_failed);
503 record_auth_for_uri (SoupAuthManagerPrivate *priv, SoupURI *uri,
504 SoupAuth *auth, gboolean prior_auth_failed)
509 char *auth_info, *old_auth_info;
512 host = get_auth_host_for_uri (priv, uri);
513 auth_info = soup_auth_get_info (auth);
515 if (!host->auth_realms) {
516 host->auth_realms = soup_path_map_new (g_free);
517 host->auths = g_hash_table_new_full (g_str_hash, g_str_equal,
518 g_free, g_object_unref);
521 /* Record where this auth realm is used. */
522 pspace = soup_auth_get_protection_space (auth, uri);
523 for (p = pspace; p; p = p->next) {
525 old_auth_info = soup_path_map_lookup (host->auth_realms, path);
527 if (!strcmp (old_auth_info, auth_info))
529 soup_path_map_remove (host->auth_realms, path);
532 soup_path_map_add (host->auth_realms, path,
533 g_strdup (auth_info));
535 soup_auth_free_protection_space (auth, pspace);
537 /* Now, make sure the auth is recorded. (If there's a
538 * pre-existing good auth, we keep that rather than the new one,
539 * since the old one might already be authenticated.)
541 old_auth = g_hash_table_lookup (host->auths, auth_info);
542 if (old_auth && (old_auth != auth || !prior_auth_failed)) {
546 g_hash_table_insert (host->auths, auth_info,
547 g_object_ref (auth));
553 auth_got_headers (SoupMessage *msg, gpointer manager)
555 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
556 SoupAuth *auth, *prior_auth, *new_auth;
557 gboolean prior_auth_failed = FALSE;
559 g_mutex_lock (&priv->lock);
561 /* See if we used auth last time */
562 prior_auth = soup_message_get_auth (msg);
563 if (prior_auth && check_auth (msg, prior_auth)) {
564 auth = g_object_ref (prior_auth);
565 if (!soup_auth_is_ready (auth, msg))
566 prior_auth_failed = TRUE;
568 auth = create_auth (priv, msg);
570 g_mutex_unlock (&priv->lock);
575 new_auth = record_auth_for_uri (priv, soup_message_get_uri (msg),
576 auth, prior_auth_failed);
577 g_object_unref (auth);
579 /* If we need to authenticate, try to do it. */
580 authenticate_auth (manager, new_auth, msg,
581 prior_auth_failed, FALSE, TRUE);
582 g_mutex_unlock (&priv->lock);
586 auth_got_body (SoupMessage *msg, gpointer manager)
588 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
591 g_mutex_lock (&priv->lock);
592 auth = lookup_auth (priv, msg);
593 if (auth && soup_auth_is_ready (auth, msg)) {
594 if (SOUP_IS_CONNECTION_AUTH (auth)) {
595 SoupMessageFlags flags;
597 flags = soup_message_get_flags (msg);
598 soup_message_set_flags (msg, flags & ~SOUP_MESSAGE_NEW_CONNECTION);
601 soup_session_requeue_message (priv->session, msg);
603 g_mutex_unlock (&priv->lock);
607 proxy_auth_got_headers (SoupMessage *msg, gpointer manager)
609 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
610 SoupAuth *prior_auth;
611 gboolean prior_auth_failed = FALSE;
613 g_mutex_lock (&priv->lock);
615 /* See if we used auth last time */
616 prior_auth = soup_message_get_proxy_auth (msg);
617 if (prior_auth && check_auth (msg, prior_auth)) {
618 if (!soup_auth_is_ready (prior_auth, msg))
619 prior_auth_failed = TRUE;
622 if (!priv->proxy_auth) {
623 priv->proxy_auth = create_auth (priv, msg);
624 if (!priv->proxy_auth) {
625 g_mutex_unlock (&priv->lock);
630 /* If we need to authenticate, try to do it. */
631 authenticate_auth (manager, priv->proxy_auth, msg,
632 prior_auth_failed, TRUE, TRUE);
633 g_mutex_unlock (&priv->lock);
637 proxy_auth_got_body (SoupMessage *msg, gpointer manager)
639 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
642 g_mutex_lock (&priv->lock);
643 auth = priv->proxy_auth;
645 if (auth && soup_auth_is_ready (auth, msg))
646 soup_session_requeue_message (priv->session, msg);
647 g_mutex_unlock (&priv->lock);
651 soup_auth_manager_request_queued (SoupSessionFeature *manager,
652 SoupSession *session,
655 soup_message_add_status_code_handler (
656 msg, "got_headers", SOUP_STATUS_UNAUTHORIZED,
657 G_CALLBACK (auth_got_headers), manager);
658 soup_message_add_status_code_handler (
659 msg, "got_body", SOUP_STATUS_UNAUTHORIZED,
660 G_CALLBACK (auth_got_body), manager);
662 soup_message_add_status_code_handler (
663 msg, "got_headers", SOUP_STATUS_PROXY_UNAUTHORIZED,
664 G_CALLBACK (proxy_auth_got_headers), manager);
665 soup_message_add_status_code_handler (
666 msg, "got_body", SOUP_STATUS_PROXY_UNAUTHORIZED,
667 G_CALLBACK (proxy_auth_got_body), manager);
671 soup_auth_manager_request_started (SoupSessionFeature *feature,
672 SoupSession *session,
676 SoupAuthManager *manager = SOUP_AUTH_MANAGER (feature);
677 SoupAuthManagerPrivate *priv = manager->priv;
680 g_mutex_lock (&priv->lock);
682 auth = lookup_auth (priv, msg);
684 authenticate_auth (manager, auth, msg, FALSE, FALSE, FALSE);
685 if (!soup_auth_is_ready (auth, msg))
688 soup_message_set_auth (msg, auth);
690 auth = priv->proxy_auth;
692 authenticate_auth (manager, auth, msg, FALSE, TRUE, FALSE);
693 if (!soup_auth_is_ready (auth, msg))
696 soup_message_set_proxy_auth (msg, auth);
698 g_mutex_unlock (&priv->lock);
702 soup_auth_manager_request_unqueued (SoupSessionFeature *manager,
703 SoupSession *session,
706 g_signal_handlers_disconnect_matched (msg, G_SIGNAL_MATCH_DATA,
707 0, 0, NULL, NULL, manager);
711 * soup_auth_manager_use_auth:
712 * @manager: a #SoupAuthManager
713 * @uri: the #SoupURI under which @auth is to be used
714 * @auth: the #SoupAuth to use
716 * Records that @auth is to be used under @uri, as though a
717 * WWW-Authenticate header had been received at that URI. This can be
718 * used to "preload" @manager's auth cache, to avoid an extra HTTP
719 * round trip in the case where you know ahead of time that a 401
720 * response will be returned.
722 * This is only useful for authentication types where the initial
723 * Authorization header does not depend on any additional information
724 * from the server. (Eg, Basic or NTLM, but not Digest.)
729 soup_auth_manager_use_auth (SoupAuthManager *manager,
733 SoupAuthManagerPrivate *priv = manager->priv;
735 g_mutex_lock (&priv->lock);
736 record_auth_for_uri (priv, uri, auth, FALSE);
737 g_mutex_unlock (&priv->lock);
741 soup_auth_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface,
742 gpointer interface_data)
744 soup_session_feature_default_interface =
745 g_type_default_interface_peek (SOUP_TYPE_SESSION_FEATURE);
747 feature_interface->attach = soup_auth_manager_attach;
748 feature_interface->request_queued = soup_auth_manager_request_queued;
749 feature_interface->request_started = soup_auth_manager_request_started;
750 feature_interface->request_unqueued = soup_auth_manager_request_unqueued;
751 feature_interface->add_feature = soup_auth_manager_add_feature;
752 feature_interface->remove_feature = soup_auth_manager_remove_feature;
753 feature_interface->has_feature = soup_auth_manager_has_feature;