Use g_clear_object(), g_clear_pointer()
[platform/upstream/libsoup.git] / libsoup / soup-auth-manager.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * soup-auth-manager.c: SoupAuth manager for SoupSession
4  *
5  * Copyright (C) 2007 Red Hat, Inc.
6  */
7
8 #ifdef HAVE_CONFIG_H
9 #include <config.h>
10 #endif
11
12 #include <string.h>
13
14 #include "soup-auth-manager.h"
15 #include "soup-address.h"
16 #include "soup-headers.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.h"
22 #include "soup-session-feature.h"
23 #include "soup-session-private.h"
24 #include "soup-uri.h"
25
26 static void soup_auth_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
27 static SoupSessionFeatureInterface *soup_session_feature_default_interface;
28
29 static void attach (SoupSessionFeature *feature, SoupSession *session);
30 static void request_queued  (SoupSessionFeature *feature, SoupSession *session,
31                              SoupMessage *msg);
32 static void request_started  (SoupSessionFeature *feature, SoupSession *session,
33                               SoupMessage *msg, SoupSocket *socket);
34 static void request_unqueued  (SoupSessionFeature *feature,
35                                SoupSession *session, SoupMessage *msg);
36 static gboolean add_feature (SoupSessionFeature *feature, GType type);
37 static gboolean remove_feature (SoupSessionFeature *feature, GType type);
38 static gboolean has_feature (SoupSessionFeature *feature, GType type);
39
40 enum {
41         AUTHENTICATE,
42         LAST_SIGNAL
43 };
44
45 static guint signals[LAST_SIGNAL] = { 0 };
46
47 G_DEFINE_TYPE_WITH_CODE (SoupAuthManager, soup_auth_manager, G_TYPE_OBJECT,
48                          G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
49                                                 soup_auth_manager_session_feature_init))
50
51 typedef struct {
52         SoupSession *session;
53         GPtrArray *auth_types;
54
55         SoupAuth *proxy_auth;
56         GHashTable *auth_hosts;
57 } SoupAuthManagerPrivate;
58 #define SOUP_AUTH_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_AUTH_MANAGER, SoupAuthManagerPrivate))
59
60 typedef struct {
61         SoupURI     *uri;
62         SoupPathMap *auth_realms;      /* path -> scheme:realm */
63         GHashTable  *auths;            /* scheme:realm -> SoupAuth */
64 } SoupAuthHost;
65
66 static void soup_auth_host_free (SoupAuthHost *host);
67
68 static void
69 soup_auth_manager_init (SoupAuthManager *manager)
70 {
71         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
72
73         priv->auth_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref);
74         priv->auth_hosts = g_hash_table_new_full (soup_uri_host_hash,
75                                                   soup_uri_host_equal,
76                                                   NULL,
77                                                   (GDestroyNotify)soup_auth_host_free);
78 }
79
80 static void
81 finalize (GObject *object)
82 {
83         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (object);
84
85         g_ptr_array_free (priv->auth_types, TRUE);
86
87         g_hash_table_destroy (priv->auth_hosts);
88
89         g_clear_object (&priv->proxy_auth);
90
91         G_OBJECT_CLASS (soup_auth_manager_parent_class)->finalize (object);
92 }
93
94 static void
95 soup_auth_manager_class_init (SoupAuthManagerClass *auth_manager_class)
96 {
97         GObjectClass *object_class = G_OBJECT_CLASS (auth_manager_class);
98
99         g_type_class_add_private (auth_manager_class, sizeof (SoupAuthManagerPrivate));
100
101         object_class->finalize = finalize;
102
103         signals[AUTHENTICATE] =
104                 g_signal_new ("authenticate",
105                               G_OBJECT_CLASS_TYPE (object_class),
106                               G_SIGNAL_RUN_FIRST,
107                               G_STRUCT_OFFSET (SoupAuthManagerClass, authenticate),
108                               NULL, NULL,
109                               _soup_marshal_NONE__OBJECT_OBJECT_BOOLEAN,
110                               G_TYPE_NONE, 3,
111                               SOUP_TYPE_MESSAGE,
112                               SOUP_TYPE_AUTH,
113                               G_TYPE_BOOLEAN);
114
115 }
116
117 static void
118 soup_auth_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface,
119                                         gpointer interface_data)
120 {
121         soup_session_feature_default_interface =
122                 g_type_default_interface_peek (SOUP_TYPE_SESSION_FEATURE);
123
124         feature_interface->attach = attach;
125         feature_interface->request_queued = request_queued;
126         feature_interface->request_started = request_started;
127         feature_interface->request_unqueued = request_unqueued;
128         feature_interface->add_feature = add_feature;
129         feature_interface->remove_feature = remove_feature;
130         feature_interface->has_feature = has_feature;
131 }
132
133 static int
134 auth_type_compare_func (gconstpointer a, gconstpointer b)
135 {
136         SoupAuthClass **auth1 = (SoupAuthClass **)a;
137         SoupAuthClass **auth2 = (SoupAuthClass **)b;
138
139         return (*auth1)->strength - (*auth2)->strength;
140 }
141
142 static gboolean
143 add_feature (SoupSessionFeature *feature, GType type)
144 {
145         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (feature);
146         SoupAuthClass *auth_class;
147
148         if (!g_type_is_a (type, SOUP_TYPE_AUTH))
149                 return FALSE;
150
151         auth_class = g_type_class_ref (type);
152         g_ptr_array_add (priv->auth_types, auth_class);
153         g_ptr_array_sort (priv->auth_types, auth_type_compare_func);
154         return TRUE;
155 }
156
157 static gboolean
158 remove_feature (SoupSessionFeature *feature, GType type)
159 {
160         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (feature);
161         SoupAuthClass *auth_class;
162         int i;
163
164         if (!g_type_is_a (type, SOUP_TYPE_AUTH))
165                 return FALSE;
166
167         auth_class = g_type_class_peek (type);
168         for (i = 0; i < priv->auth_types->len; i++) {
169                 if (priv->auth_types->pdata[i] == (gpointer)auth_class) {
170                         g_ptr_array_remove_index (priv->auth_types, i);
171                         return TRUE;
172                 }
173         }
174
175         return FALSE;
176 }
177
178 static gboolean
179 has_feature (SoupSessionFeature *feature, GType type)
180 {
181         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (feature);
182         SoupAuthClass *auth_class;
183         int i;
184
185         if (!g_type_is_a (type, SOUP_TYPE_AUTH))
186                 return FALSE;
187
188         auth_class = g_type_class_peek (type);
189         for (i = 0; i < priv->auth_types->len; i++) {
190                 if (priv->auth_types->pdata[i] == (gpointer)auth_class)
191                         return TRUE;
192         }
193         return FALSE;
194 }
195
196 void
197 soup_auth_manager_emit_authenticate (SoupAuthManager *manager, SoupMessage *msg,
198                                      SoupAuth *auth, gboolean retrying)
199 {
200         g_signal_emit (manager, signals[AUTHENTICATE], 0, msg, auth, retrying);
201 }
202
203 static void
204 attach (SoupSessionFeature *manager, SoupSession *session)
205 {
206         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
207
208         /* FIXME: should support multiple sessions */
209         priv->session = session;
210
211         soup_session_feature_default_interface->attach (manager, session);
212 }
213
214 static inline const char *
215 auth_header_for_message (SoupMessage *msg)
216 {
217         if (msg->status_code == SOUP_STATUS_PROXY_UNAUTHORIZED) {
218                 return soup_message_headers_get_list (msg->response_headers,
219                                                       "Proxy-Authenticate");
220         } else {
221                 return soup_message_headers_get_list (msg->response_headers,
222                                                       "WWW-Authenticate");
223         }
224 }
225
226 static GSList *
227 next_challenge_start (GSList *items)
228 {
229         /* The relevant grammar (from httpbis):
230          *
231          * WWW-Authenticate   = 1#challenge
232          * Proxy-Authenticate = 1#challenge
233          * challenge          = auth-scheme [ 1*SP ( b64token / #auth-param ) ]
234          * auth-scheme        = token
235          * auth-param         = token BWS "=" BWS ( token / quoted-string )
236          * b64token           = 1*( ALPHA / DIGIT /
237          *                          "-" / "." / "_" / "~" / "+" / "/" ) *"="
238          *
239          * The fact that quoted-strings can contain commas, equals
240          * signs, and auth scheme names makes it tricky to "cheat" on
241          * the parsing. So soup_auth_manager_extract_challenge() will
242          * have used soup_header_parse_list() to split the header into
243          * items. Given the grammar above, the possible items are:
244          *
245          *   auth-scheme
246          *   auth-scheme 1*SP b64token
247          *   auth-scheme 1*SP auth-param
248          *   auth-param
249          *
250          * where the first three represent the start of a new challenge and
251          * the last one does not.
252          */
253
254         for (; items; items = items->next) {
255                 const char *item = items->data;
256                 const char *sp = strpbrk (item, "\t\r\n ");
257                 const char *eq = strchr (item, '=');
258
259                 if (!eq) {
260                         /* No "=", so it can't be an auth-param */
261                         return items;
262                 }
263                 if (!sp || sp > eq) {
264                         /* No space, or first space appears after the "=",
265                          * so it must be an auth-param.
266                          */
267                         continue;
268                 }
269                 while (g_ascii_isspace (*++sp))
270                         ;
271                 if (sp == eq) {
272                         /* First "=" appears immediately after the first
273                          * space, so this must be an auth-param with
274                          * space around the "=".
275                          */
276                         continue;
277                 }
278
279                 /* "auth-scheme auth-param" or "auth-scheme b64token" */
280                 return items;
281         }
282
283         return NULL;
284 }
285
286 char *
287 soup_auth_manager_extract_challenge (const char *challenges, const char *scheme)
288 {
289         GSList *items, *i, *next;
290         int schemelen = strlen (scheme);
291         char *item;
292         GString *challenge;
293
294         items = soup_header_parse_list (challenges);
295
296         /* First item will start with the scheme name, followed by
297          * either nothing, or else a space and then the first
298          * auth-param.
299          */
300         for (i = items; i; i = next_challenge_start (i->next)) {
301                 item = i->data;
302                 if (!g_ascii_strncasecmp (item, scheme, schemelen) &&
303                     (!item[schemelen] || g_ascii_isspace (item[schemelen])))
304                         break;
305         }
306         if (!i) {
307                 soup_header_free_list (items);
308                 return NULL;
309         }
310
311         next = next_challenge_start (i->next);
312         challenge = g_string_new (item);
313         for (i = i->next; i != next; i = i->next) {
314                 item = i->data;
315                 g_string_append (challenge, ", ");
316                 g_string_append (challenge, item);
317         }
318
319         soup_header_free_list (items);
320         return g_string_free (challenge, FALSE);
321 }
322
323 static SoupAuth *
324 create_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
325 {
326         const char *header;
327         SoupAuthClass *auth_class;
328         char *challenge = NULL;
329         SoupAuth *auth;
330         int i;
331
332         header = auth_header_for_message (msg);
333         if (!header)
334                 return NULL;
335
336         for (i = priv->auth_types->len - 1; i >= 0; i--) {
337                 auth_class = priv->auth_types->pdata[i];
338                 challenge = soup_auth_manager_extract_challenge (header, auth_class->scheme_name);
339                 if (challenge)
340                         break;
341         }
342         if (!challenge)
343                 return NULL;
344
345         auth = soup_auth_new (G_TYPE_FROM_CLASS (auth_class), msg, challenge);
346         g_free (challenge);
347         return auth;
348 }
349
350 static gboolean
351 check_auth (SoupMessage *msg, SoupAuth *auth)
352 {
353         const char *header;
354         char *challenge;
355         gboolean ok;
356
357         header = auth_header_for_message (msg);
358         if (!header)
359                 return FALSE;
360
361         challenge = soup_auth_manager_extract_challenge (header, soup_auth_get_scheme_name (auth));
362         if (!challenge)
363                 return FALSE;
364
365         ok = soup_auth_update (auth, msg, challenge);
366         g_free (challenge);
367         return ok;
368 }
369
370 static SoupAuthHost *
371 get_auth_host_for_message (SoupAuthManagerPrivate *priv, SoupMessage *msg)
372 {
373         SoupAuthHost *host;
374         SoupURI *uri = soup_message_get_uri (msg);
375
376         host = g_hash_table_lookup (priv->auth_hosts, uri);
377         if (host)
378                 return host;
379
380         host = g_slice_new0 (SoupAuthHost);
381         host->uri = soup_uri_copy_host (uri);
382         g_hash_table_insert (priv->auth_hosts, host->uri, host);
383
384         return host;
385 }
386
387 static void
388 soup_auth_host_free (SoupAuthHost *host)
389 {
390         g_clear_pointer (&host->auth_realms, soup_path_map_free);
391         g_clear_pointer (&host->auths, g_hash_table_destroy);
392
393         soup_uri_free (host->uri);
394         g_slice_free (SoupAuthHost, host);
395 }
396
397 static SoupAuth *
398 lookup_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
399 {
400         SoupAuthHost *host;
401         const char *path, *realm;
402
403         host = get_auth_host_for_message (priv, msg);
404         if (!host->auth_realms)
405                 return NULL;
406
407         path = soup_message_get_uri (msg)->path;
408         if (!path)
409                 path = "/";
410         realm = soup_path_map_lookup (host->auth_realms, path);
411         if (realm)
412                 return g_hash_table_lookup (host->auths, realm);
413         else
414                 return NULL;
415 }
416
417 static gboolean
418 authenticate_auth (SoupAuthManager *manager, SoupAuth *auth,
419                    SoupMessage *msg, gboolean prior_auth_failed,
420                    gboolean proxy, gboolean can_interact)
421 {
422         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
423         SoupURI *uri;
424
425         if (proxy) {
426                 SoupMessageQueue *queue;
427                 SoupMessageQueueItem *item;
428
429                 queue = soup_session_get_queue (priv->session);
430                 item = soup_message_queue_lookup (queue, msg);
431                 if (item) {
432                         uri = soup_connection_get_proxy_uri (item->conn);
433                         soup_message_queue_item_unref (item);
434                 } else
435                         uri = NULL;
436
437                 if (!uri)
438                         return FALSE;
439         } else
440                 uri = soup_message_get_uri (msg);
441
442         /* If a password is specified explicitly in the URI, use it
443          * even if the auth had previously already been authenticated.
444          */
445         if (uri->password) {
446                 if (!prior_auth_failed)
447                         soup_auth_authenticate (auth, uri->user, uri->password);
448         } else if (!soup_auth_is_authenticated (auth) && can_interact) {
449                 soup_auth_manager_emit_authenticate (manager, msg, auth,
450                                                      prior_auth_failed);
451         }
452
453         return soup_auth_is_authenticated (auth);
454 }
455
456 static void
457 update_auth (SoupMessage *msg, gpointer manager)
458 {
459         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
460         SoupAuthHost *host;
461         SoupAuth *auth, *prior_auth, *old_auth;
462         const char *path;
463         char *auth_info, *old_auth_info;
464         GSList *pspace, *p;
465         gboolean prior_auth_failed = FALSE;
466
467         host = get_auth_host_for_message (priv, msg);
468
469         /* See if we used auth last time */
470         prior_auth = soup_message_get_auth (msg);
471         if (prior_auth && check_auth (msg, prior_auth)) {
472                 auth = prior_auth;
473                 if (!soup_auth_is_authenticated (auth))
474                         prior_auth_failed = TRUE;
475         } else {
476                 auth = create_auth (priv, msg);
477                 if (!auth)
478                         return;
479         }
480         auth_info = soup_auth_get_info (auth);
481
482         if (!host->auth_realms) {
483                 host->auth_realms = soup_path_map_new (g_free);
484                 host->auths = g_hash_table_new_full (g_str_hash, g_str_equal,
485                                                      g_free, g_object_unref);
486         }
487
488         /* Record where this auth realm is used. */
489         pspace = soup_auth_get_protection_space (auth, soup_message_get_uri (msg));
490         for (p = pspace; p; p = p->next) {
491                 path = p->data;
492                 old_auth_info = soup_path_map_lookup (host->auth_realms, path);
493                 if (old_auth_info) {
494                         if (!strcmp (old_auth_info, auth_info))
495                                 continue;
496                         soup_path_map_remove (host->auth_realms, path);
497                 }
498
499                 soup_path_map_add (host->auth_realms, path,
500                                    g_strdup (auth_info));
501         }
502         soup_auth_free_protection_space (auth, pspace);
503
504         /* Now, make sure the auth is recorded. (If there's a
505          * pre-existing auth, we keep that rather than the new one,
506          * since the old one might already be authenticated.)
507          */
508         old_auth = g_hash_table_lookup (host->auths, auth_info);
509         if (old_auth) {
510                 g_free (auth_info);
511                 if (auth != old_auth && auth != prior_auth) {
512                         g_object_unref (auth);
513                         auth = old_auth;
514                 }
515         } else {
516                 g_hash_table_insert (host->auths, auth_info, auth);
517         }
518
519         /* If we need to authenticate, try to do it. */
520         authenticate_auth (manager, auth, msg,
521                            prior_auth_failed, FALSE, TRUE);
522 }
523
524 static void
525 requeue_if_authenticated (SoupMessage *msg, gpointer manager)
526 {
527         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
528         SoupAuth *auth = lookup_auth (priv, msg);
529
530         if (auth && soup_auth_is_authenticated (auth))
531                 soup_session_requeue_message (priv->session, msg);
532 }
533
534 static void
535 update_proxy_auth (SoupMessage *msg, gpointer manager)
536 {
537         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
538         SoupAuth *prior_auth;
539         gboolean prior_auth_failed = FALSE;
540
541         /* See if we used auth last time */
542         prior_auth = soup_message_get_proxy_auth (msg);
543         if (prior_auth && check_auth (msg, prior_auth)) {
544                 if (!soup_auth_is_authenticated (prior_auth))
545                         prior_auth_failed = TRUE;
546         }
547
548         if (!priv->proxy_auth) {
549                 priv->proxy_auth = create_auth (priv, msg);
550                 if (!priv->proxy_auth)
551                         return;
552         }
553
554         /* If we need to authenticate, try to do it. */
555         authenticate_auth (manager, priv->proxy_auth, msg,
556                            prior_auth_failed, TRUE, TRUE);
557 }
558
559 static void
560 requeue_if_proxy_authenticated (SoupMessage *msg, gpointer manager)
561 {
562         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
563         SoupAuth *auth = priv->proxy_auth;
564
565         if (auth && soup_auth_is_authenticated (auth))
566                 soup_session_requeue_message (priv->session, msg);
567 }
568
569 static void
570 request_queued (SoupSessionFeature *manager, SoupSession *session,
571                 SoupMessage *msg)
572 {
573         soup_message_add_status_code_handler (
574                 msg, "got_headers", SOUP_STATUS_UNAUTHORIZED,
575                 G_CALLBACK (update_auth), manager);
576         soup_message_add_status_code_handler (
577                 msg, "got_body", SOUP_STATUS_UNAUTHORIZED,
578                 G_CALLBACK (requeue_if_authenticated), manager);
579
580         soup_message_add_status_code_handler (
581                 msg, "got_headers", SOUP_STATUS_PROXY_UNAUTHORIZED,
582                 G_CALLBACK (update_proxy_auth), manager);
583         soup_message_add_status_code_handler (
584                 msg, "got_body", SOUP_STATUS_PROXY_UNAUTHORIZED,
585                 G_CALLBACK (requeue_if_proxy_authenticated), manager);
586 }
587
588 static void
589 request_started (SoupSessionFeature *feature, SoupSession *session,
590                  SoupMessage *msg, SoupSocket *socket)
591 {
592         SoupAuthManager *manager = SOUP_AUTH_MANAGER (feature);
593         SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER_GET_PRIVATE (manager);
594         SoupAuth *auth;
595
596         auth = lookup_auth (priv, msg);
597         if (!auth || !authenticate_auth (manager, auth, msg, FALSE, FALSE, FALSE))
598                 auth = NULL;
599         soup_message_set_auth (msg, auth);
600
601         auth = priv->proxy_auth;
602         if (!auth || !authenticate_auth (manager, auth, msg, FALSE, TRUE, FALSE))
603                 auth = NULL;
604         soup_message_set_proxy_auth (msg, auth);
605 }
606
607 static void
608 request_unqueued (SoupSessionFeature *manager, SoupSession *session,
609                   SoupMessage *msg)
610 {
611         g_signal_handlers_disconnect_matched (msg, G_SIGNAL_MATCH_DATA,
612                                               0, 0, NULL, NULL, manager);
613 }