1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * soup-auth-negotiate.c: HTTP Negotiate Authentication helper
4 *
5 * Copyright (C) 2009,2013 Guido Guenther <agx@sigxcpu.org>
6 * Copyright (C) 2016 Red Hat, Inc.
7 */
8
9 #ifdef HAVE_CONFIG_H
10 #include <config.h>
11 #endif
12
13 #include <string.h>
14
15 #ifdef LIBSOUP_HAVE_GSSAPI
16 #include <gssapi/gssapi.h>
17 #endif /* LIBSOUP_HAVE_GSSAPI */
18
19 #include "soup-auth-negotiate.h"
20 #include "soup-headers.h"
21 #include "soup-message.h"
22 #include "soup-message-private.h"
23 #include "soup-misc-private.h"
24 #include "soup-uri.h"
25
26 /**
27 * soup_auth_negotiate_supported:
28 *
29 * Indicates whether libsoup was built with GSSAPI support. If this is
30 * %FALSE, %SOUP_TYPE_AUTH_NEGOTIATE will still be defined and can
31 * still be added to a #SoupSession, but libsoup will never attempt to
32 * actually use this auth type.
33 *
34 * Since: 2.54
35 */
36 gboolean
soup_auth_negotiate_supported(void)37 soup_auth_negotiate_supported (void)
38 {
39 #ifdef LIBSOUP_HAVE_GSSAPI
40 return TRUE;
41 #else
42 return FALSE;
43 #endif
44 }
45
46 #define AUTH_GSS_ERROR -1
47 #define AUTH_GSS_COMPLETE 1
48 #define AUTH_GSS_CONTINUE 0
49
50 typedef enum {
51 SOUP_NEGOTIATE_NEW,
52 SOUP_NEGOTIATE_RECEIVED_CHALLENGE, /* received initial negotiate header */
53 SOUP_NEGOTIATE_SENT_RESPONSE, /* sent response to server */
54 SOUP_NEGOTIATE_FAILED
55 } SoupNegotiateState;
56
57 typedef struct {
58 gboolean initialized;
59 gchar *response_header;
60
61 #ifdef LIBSOUP_HAVE_GSSAPI
62 gss_ctx_id_t context;
63 gss_name_t server_name;
64 #endif
65
66 SoupNegotiateState state;
67 } SoupNegotiateConnectionState;
68
69 typedef struct {
70 gboolean is_authenticated;
71 } SoupAuthNegotiatePrivate;
72
73 /**
74 * SOUP_TYPE_AUTH_NEGOTIATE:
75 *
76 * A #GType corresponding to HTTP-based GSS-Negotiate authentication.
77 * #SoupSessions do not support this type by default; if you want to
78 * enable support for it, call soup_session_add_feature_by_type(),
79 * passing %SOUP_TYPE_AUTH_NEGOTIATE.
80 *
81 * This auth type will only work if libsoup was compiled with GSSAPI
82 * support; you can check soup_auth_negotiate_supported() to see if it
83 * was.
84 *
85 * Since: 2.54
86 */
87 G_DEFINE_TYPE_WITH_PRIVATE (SoupAuthNegotiate, soup_auth_negotiate, SOUP_TYPE_CONNECTION_AUTH)
88
89 #ifdef LIBSOUP_HAVE_GSSAPI
90 static gboolean check_auth_trusted_uri (SoupConnectionAuth *auth,
91 SoupMessage *msg);
92 static gboolean soup_gss_build_response (SoupNegotiateConnectionState *conn,
93 SoupAuth *auth, GError **err);
94 static void soup_gss_client_cleanup (SoupNegotiateConnectionState *conn);
95 static gboolean soup_gss_client_init (SoupNegotiateConnectionState *conn,
96 const char *host, GError **err);
97 static int soup_gss_client_step (SoupNegotiateConnectionState *conn,
98 const char *host, GError **err);
99
100 static GSList *trusted_uris;
101 static GSList *blacklisted_uris;
102
103 static void parse_uris_from_env_variable (const gchar *env_variable, GSList **list);
104
105 static void check_server_response (SoupMessage *msg, gpointer auth);
106
107 static const char spnego_OID[] = "\x2b\x06\x01\x05\x05\x02";
108 static const gss_OID_desc gss_mech_spnego = { sizeof (spnego_OID) - 1, (void *) &spnego_OID };
109
110 static gpointer
soup_auth_negotiate_create_connection_state(SoupConnectionAuth * auth)111 soup_auth_negotiate_create_connection_state (SoupConnectionAuth *auth)
112 {
113 SoupNegotiateConnectionState *conn;
114
115 conn = g_slice_new0 (SoupNegotiateConnectionState);
116 conn->state = SOUP_NEGOTIATE_NEW;
117
118 return conn;
119 }
120
121 static void
free_connection_state_data(SoupNegotiateConnectionState * conn)122 free_connection_state_data (SoupNegotiateConnectionState *conn)
123 {
124 soup_gss_client_cleanup (conn);
125 g_free (conn->response_header);
126 }
127
128 static void
soup_auth_negotiate_free_connection_state(SoupConnectionAuth * auth,gpointer state)129 soup_auth_negotiate_free_connection_state (SoupConnectionAuth *auth,
130 gpointer state)
131 {
132 SoupNegotiateConnectionState *conn = state;
133
134 free_connection_state_data (conn);
135
136 g_slice_free (SoupNegotiateConnectionState, conn);
137 }
138
139 static GSList *
soup_auth_negotiate_get_protection_space(SoupAuth * auth,SoupURI * source_uri)140 soup_auth_negotiate_get_protection_space (SoupAuth *auth, SoupURI *source_uri)
141 {
142 char *space, *p;
143
144 space = g_strdup (source_uri->path);
145
146 /* Strip filename component */
147 p = strrchr (space, '/');
148 if (p && p == space && p[1])
149 p[1] = '\0';
150 else if (p && p[1])
151 *p = '\0';
152
153 return g_slist_prepend (NULL, space);
154 }
155
156 static void
soup_auth_negotiate_authenticate(SoupAuth * auth,const char * username,const char * password)157 soup_auth_negotiate_authenticate (SoupAuth *auth, const char *username,
158 const char *password)
159 {
160 SoupAuthNegotiate *negotiate = SOUP_AUTH_NEGOTIATE (auth);
161 SoupAuthNegotiatePrivate *priv = soup_auth_negotiate_get_instance_private (negotiate);
162
163 /* It is not possible to authenticate with username and password. */
164 priv->is_authenticated = FALSE;
165 }
166
167 static gboolean
soup_auth_negotiate_is_authenticated(SoupAuth * auth)168 soup_auth_negotiate_is_authenticated (SoupAuth *auth)
169 {
170 SoupAuthNegotiate *negotiate = SOUP_AUTH_NEGOTIATE (auth);
171 SoupAuthNegotiatePrivate *priv = soup_auth_negotiate_get_instance_private (negotiate);
172
173 /* We are authenticated just in case we received the GSS_S_COMPLETE. */
174 return priv->is_authenticated;
175 }
176
177 static gboolean
soup_auth_negotiate_can_authenticate(SoupAuth * auth)178 soup_auth_negotiate_can_authenticate (SoupAuth *auth)
179 {
180 return FALSE;
181 }
182
183 static char *
soup_auth_negotiate_get_connection_authorization(SoupConnectionAuth * auth,SoupMessage * msg,gpointer state)184 soup_auth_negotiate_get_connection_authorization (SoupConnectionAuth *auth,
185 SoupMessage *msg,
186 gpointer state)
187 {
188 SoupNegotiateConnectionState *conn = state;
189 char *header = NULL;
190
191 if (conn->state == SOUP_NEGOTIATE_NEW) {
192 GError *err = NULL;
193
194 if (!check_auth_trusted_uri (auth, msg)) {
195 conn->state = SOUP_NEGOTIATE_FAILED;
196 return NULL;
197 }
198
199 if (!soup_gss_build_response (conn, SOUP_AUTH (auth), &err)) {
200 /* FIXME: report further upward via
201 * soup_message_get_error_message */
202 if (conn->initialized)
203 g_warning ("gssapi step failed: %s", err->message);
204 else
205 g_warning ("gssapi init failed: %s", err->message);
206 conn->state = SOUP_NEGOTIATE_FAILED;
207 g_clear_error (&err);
208
209 return NULL;
210 }
211 }
212
213 if (conn->response_header) {
214 header = conn->response_header;
215 conn->response_header = NULL;
216 conn->state = SOUP_NEGOTIATE_SENT_RESPONSE;
217 }
218
219 return header;
220 }
221
222 static gboolean
soup_auth_negotiate_is_connection_ready(SoupConnectionAuth * auth,SoupMessage * msg,gpointer state)223 soup_auth_negotiate_is_connection_ready (SoupConnectionAuth *auth,
224 SoupMessage *msg,
225 gpointer state)
226 {
227 SoupNegotiateConnectionState *conn = state;
228
229 return conn->state != SOUP_NEGOTIATE_FAILED;
230 }
231 #endif /* LIBSOUP_HAVE_GSSAPI */
232
233 static gboolean
soup_auth_negotiate_update_connection(SoupConnectionAuth * auth,SoupMessage * msg,const char * header,gpointer state)234 soup_auth_negotiate_update_connection (SoupConnectionAuth *auth, SoupMessage *msg,
235 const char *header, gpointer state)
236 {
237 #ifdef LIBSOUP_HAVE_GSSAPI
238 gboolean success = TRUE;
239 SoupNegotiateConnectionState *conn = state;
240 GError *err = NULL;
241
242 if (!check_auth_trusted_uri (auth, msg)) {
243 conn->state = SOUP_NEGOTIATE_FAILED;
244 goto out;
245 }
246
247 /* Found negotiate header with no token, start negotiate */
248 if (strcmp (header, "Negotiate") == 0) {
249 /* If we were already negotiating and we get a 401
250 * with no token, start again. */
251 if (conn->state == SOUP_NEGOTIATE_SENT_RESPONSE) {
252 free_connection_state_data (conn);
253 conn->initialized = FALSE;
254 }
255
256 conn->state = SOUP_NEGOTIATE_RECEIVED_CHALLENGE;
257 if (soup_gss_build_response (conn, SOUP_AUTH (auth), &err)) {
258 /* Connect the signal only once per message */
259 if (!g_object_get_data (G_OBJECT (msg), "negotiate-got-headers-connected")) {
260 /* Wait for the 2xx response to verify server response */
261 g_signal_connect_data (msg,
262 "got_headers",
263 G_CALLBACK (check_server_response),
264 g_object_ref (auth),
265 (GClosureNotify) g_object_unref,
266 0);
267 /* Mark that the signal was connected */
268 g_object_set_data (G_OBJECT (msg),
269 "negotiate-got-headers-connected",
270 GINT_TO_POINTER (1));
271 }
272 goto out;
273 } else {
274 /* FIXME: report further upward via
275 * soup_message_get_error_message */
276 if (conn->initialized)
277 g_warning ("gssapi step failed: %s", err->message);
278 else
279 g_warning ("gssapi init failed: %s", err->message);
280 success = FALSE;
281 }
282 } else if (!strncmp (header, "Negotiate ", 10)) {
283 if (soup_gss_client_step (conn, header + 10, &err) == AUTH_GSS_CONTINUE) {
284 conn->state = SOUP_NEGOTIATE_RECEIVED_CHALLENGE;
285 goto out;
286 }
287 }
288
289 conn->state = SOUP_NEGOTIATE_FAILED;
290 out:
291 g_clear_error (&err);
292 return success;
293 #else
294 return FALSE;
295 #endif /* LIBSOUP_HAVE_GSSAPI */
296 }
297
298 static void
soup_auth_negotiate_init(SoupAuthNegotiate * negotiate)299 soup_auth_negotiate_init (SoupAuthNegotiate *negotiate)
300 {
301 g_object_set (G_OBJECT (negotiate), SOUP_AUTH_REALM, "", NULL);
302 }
303
304 static void
soup_auth_negotiate_class_init(SoupAuthNegotiateClass * auth_negotiate_class)305 soup_auth_negotiate_class_init (SoupAuthNegotiateClass *auth_negotiate_class)
306 {
307 SoupAuthClass *auth_class = SOUP_AUTH_CLASS (auth_negotiate_class);
308 SoupConnectionAuthClass *conn_auth_class =
309 SOUP_CONNECTION_AUTH_CLASS (auth_negotiate_class);
310
311 auth_class->scheme_name = "Negotiate";
312 auth_class->strength = 0;
313
314 conn_auth_class->update_connection = soup_auth_negotiate_update_connection;
315 #ifdef LIBSOUP_HAVE_GSSAPI
316 auth_class->strength = 7;
317
318 conn_auth_class->create_connection_state = soup_auth_negotiate_create_connection_state;
319 conn_auth_class->free_connection_state = soup_auth_negotiate_free_connection_state;
320 conn_auth_class->get_connection_authorization = soup_auth_negotiate_get_connection_authorization;
321 conn_auth_class->is_connection_ready = soup_auth_negotiate_is_connection_ready;
322
323 auth_class->get_protection_space = soup_auth_negotiate_get_protection_space;
324 auth_class->authenticate = soup_auth_negotiate_authenticate;
325 auth_class->is_authenticated = soup_auth_negotiate_is_authenticated;
326 auth_class->can_authenticate = soup_auth_negotiate_can_authenticate;
327
328 parse_uris_from_env_variable ("SOUP_GSSAPI_TRUSTED_URIS", &trusted_uris);
329 parse_uris_from_env_variable ("SOUP_GSSAPI_BLACKLISTED_URIS", &blacklisted_uris);
330 #endif /* LIBSOUP_HAVE_GSSAPI */
331 }
332
333 #ifdef LIBSOUP_HAVE_GSSAPI
334 static void
check_server_response(SoupMessage * msg,gpointer auth)335 check_server_response (SoupMessage *msg, gpointer auth)
336 {
337 gint ret;
338 const char *auth_headers;
339 GError *err = NULL;
340 SoupAuthNegotiate *negotiate = auth;
341 SoupAuthNegotiatePrivate *priv = soup_auth_negotiate_get_instance_private (negotiate);
342 SoupNegotiateConnectionState *conn;
343
344 conn = soup_connection_auth_get_connection_state_for_message (SOUP_CONNECTION_AUTH (auth), msg);
345 if (!conn)
346 return;
347
348 if (auth != soup_message_get_auth (msg))
349 return;
350
351 if (msg->status_code == SOUP_STATUS_UNAUTHORIZED)
352 return;
353
354 /* FIXME: need to check for proxy-auth too */
355 auth_headers = soup_message_headers_get_one (msg->response_headers,
356 "WWW-Authenticate");
357 if (!auth_headers || g_ascii_strncasecmp (auth_headers, "Negotiate ", 10) != 0) {
358 g_warning ("Failed to parse auth header");
359 conn->state = SOUP_NEGOTIATE_FAILED;
360 goto out;
361 }
362
363 ret = soup_gss_client_step (conn, auth_headers + 10, &err);
364
365 switch (ret) {
366 case AUTH_GSS_COMPLETE:
367 priv->is_authenticated = TRUE;
368 break;
369 case AUTH_GSS_CONTINUE:
370 conn->state = SOUP_NEGOTIATE_RECEIVED_CHALLENGE;
371 break;
372 case AUTH_GSS_ERROR:
373 if (err)
374 g_warning ("%s", err->message);
375 /* Unfortunately, so many programs (curl, Firefox, ..) ignore
376 * the return token that is included in the response, so it is
377 * possible that there are servers that send back broken stuff.
378 * Try to behave in the right way (pass the token to
379 * gss_init_sec_context()), show a warning, but don't fail
380 * if the server returned 200. */
381 if (msg->status_code == SOUP_STATUS_OK)
382 priv->is_authenticated = TRUE;
383 else
384 conn->state = SOUP_NEGOTIATE_FAILED;
385 break;
386 default:
387 conn->state = SOUP_NEGOTIATE_FAILED;
388 }
389 out:
390 g_clear_error (&err);
391 }
392
393 /* Check if scheme://host:port from message matches the given URI. */
394 static gint
match_base_uri(SoupURI * list_uri,SoupURI * msg_uri)395 match_base_uri (SoupURI *list_uri, SoupURI *msg_uri)
396 {
397 if (msg_uri->scheme != list_uri->scheme)
398 return 1;
399
400 if (list_uri->port && (msg_uri->port != list_uri->port))
401 return 1;
402
403 if (list_uri->host)
404 return !soup_host_matches_host (msg_uri->host, list_uri->host);
405
406 return 0;
407 }
408
409 /* Parses a comma separated list of URIS from the environment. */
410 static void
parse_uris_from_env_variable(const gchar * env_variable,GSList ** list)411 parse_uris_from_env_variable (const gchar *env_variable, GSList **list)
412 {
413 gchar **uris = NULL;
414 const gchar *env;
415 gint i;
416 guint length;
417
418 /* Initialize the list */
419 *list = NULL;
420
421 if (!(env = g_getenv (env_variable)))
422 return;
423
424 if (!(uris = g_strsplit (env, ",", -1)))
425 return;
426
427 length = g_strv_length (uris);
428 for (i = 0; i < length; i++) {
429 SoupURI *uri;
430
431 /* If the supplied URI is valid, append it to the list */
432 if ((uri = soup_uri_new (uris[i])))
433 *list = g_slist_prepend (*list, uri);
434 }
435
436 g_strfreev (uris);
437 }
438
439 static gboolean
check_auth_trusted_uri(SoupConnectionAuth * auth,SoupMessage * msg)440 check_auth_trusted_uri (SoupConnectionAuth *auth, SoupMessage *msg)
441 {
442 SoupURI *msg_uri;
443 GSList *matched = NULL;
444
445 g_return_val_if_fail (auth != NULL, FALSE);
446 g_return_val_if_fail (msg != NULL, FALSE);
447
448 msg_uri = soup_message_get_uri (msg);
449
450 /* First check if the URI is not on blacklist */
451 if (blacklisted_uris &&
452 g_slist_find_custom (blacklisted_uris, msg_uri, (GCompareFunc) match_base_uri))
453 return FALSE;
454
455 /* If no trusted URIs are set, we allow all HTTPS URIs */
456 if (!trusted_uris)
457 return soup_uri_is_https (msg_uri, NULL);
458
459 matched = g_slist_find_custom (trusted_uris,
460 msg_uri,
461 (GCompareFunc) match_base_uri);
462
463 return matched ? TRUE : FALSE;
464 }
465
466 static gboolean
soup_gss_build_response(SoupNegotiateConnectionState * conn,SoupAuth * auth,GError ** err)467 soup_gss_build_response (SoupNegotiateConnectionState *conn, SoupAuth *auth, GError **err)
468 {
469 if (!conn->initialized)
470 if (!soup_gss_client_init (conn, soup_auth_get_host (auth), err))
471 return FALSE;
472
473 if (soup_gss_client_step (conn, "", err) != AUTH_GSS_CONTINUE)
474 return FALSE;
475
476 return TRUE;
477 }
478
479 static void
soup_gss_error(OM_uint32 err_maj,OM_uint32 err_min,GError ** err)480 soup_gss_error (OM_uint32 err_maj, OM_uint32 err_min, GError **err)
481 {
482 OM_uint32 maj_stat, min_stat, msg_ctx = 0;
483 gss_buffer_desc status;
484 gchar *buf_maj = NULL, *buf_min = NULL;
485
486 do {
487 maj_stat = gss_display_status (&min_stat,
488 err_maj,
489 GSS_C_GSS_CODE,
490 (gss_OID) &gss_mech_spnego,
491 &msg_ctx,
492 &status);
493 if (GSS_ERROR (maj_stat))
494 break;
495
496 buf_maj = g_strdup ((gchar *) status.value);
497 gss_release_buffer (&min_stat, &status);
498
499 maj_stat = gss_display_status (&min_stat,
500 err_min,
501 GSS_C_MECH_CODE,
502 GSS_C_NULL_OID,
503 &msg_ctx,
504 &status);
505 if (!GSS_ERROR (maj_stat)) {
506 buf_min = g_strdup ((gchar *) status.value);
507 gss_release_buffer (&min_stat, &status);
508 }
509
510 if (err && *err == NULL) {
511 g_set_error (err,
512 SOUP_HTTP_ERROR,
513 SOUP_STATUS_UNAUTHORIZED,
514 "%s: %s",
515 buf_maj,
516 buf_min ? buf_min : "");
517 }
518 g_free (buf_maj);
519 g_free (buf_min);
520 buf_min = buf_maj = NULL;
521 } while (!GSS_ERROR (maj_stat) && msg_ctx != 0);
522 }
523
524 static gboolean
soup_gss_client_init(SoupNegotiateConnectionState * conn,const gchar * host,GError ** err)525 soup_gss_client_init (SoupNegotiateConnectionState *conn, const gchar *host, GError **err)
526 {
527 OM_uint32 maj_stat, min_stat;
528 gchar *service = NULL;
529 gss_buffer_desc token = GSS_C_EMPTY_BUFFER;
530 gboolean ret = FALSE;
531 gchar *h;
532
533 conn->server_name = GSS_C_NO_NAME;
534 conn->context = GSS_C_NO_CONTEXT;
535
536 h = g_ascii_strdown (host, -1);
537 service = g_strconcat ("HTTP@", h, NULL);
538 token.length = strlen (service);
539 token.value = (gchar *) service;
540
541 maj_stat = gss_import_name (&min_stat,
542 &token,
543 (gss_OID) GSS_C_NT_HOSTBASED_SERVICE,
544 &conn->server_name);
545
546 if (GSS_ERROR (maj_stat)) {
547 soup_gss_error (maj_stat, min_stat, err);
548 ret = FALSE;
549 goto out;
550 }
551
552 conn->initialized = TRUE;
553 ret = TRUE;
554 out:
555 g_free (h);
556 g_free (service);
557 return ret;
558 }
559
560 static gint
soup_gss_client_step(SoupNegotiateConnectionState * conn,const gchar * challenge,GError ** err)561 soup_gss_client_step (SoupNegotiateConnectionState *conn, const gchar *challenge, GError **err)
562 {
563 OM_uint32 maj_stat, min_stat;
564 gss_buffer_desc in = GSS_C_EMPTY_BUFFER;
565 gss_buffer_desc out = GSS_C_EMPTY_BUFFER;
566 gint ret = AUTH_GSS_CONTINUE;
567
568 g_clear_pointer (&conn->response_header, g_free);
569
570 if (challenge && *challenge) {
571 size_t len;
572 in.value = g_base64_decode (challenge, &len);
573 in.length = len;
574 }
575
576 maj_stat = gss_init_sec_context (&min_stat,
577 GSS_C_NO_CREDENTIAL,
578 &conn->context,
579 conn->server_name,
580 (gss_OID) &gss_mech_spnego,
581 GSS_C_MUTUAL_FLAG,
582 GSS_C_INDEFINITE,
583 GSS_C_NO_CHANNEL_BINDINGS,
584 &in,
585 NULL,
586 &out,
587 NULL,
588 NULL);
589
590 if ((maj_stat != GSS_S_COMPLETE) && (maj_stat != GSS_S_CONTINUE_NEEDED)) {
591 soup_gss_error (maj_stat, min_stat, err);
592 ret = AUTH_GSS_ERROR;
593 goto out;
594 }
595
596 ret = (maj_stat == GSS_S_COMPLETE) ? AUTH_GSS_COMPLETE : AUTH_GSS_CONTINUE;
597 if (out.length) {
598 gchar *response = g_base64_encode ((const guchar *) out.value, out.length);
599 conn->response_header = g_strconcat ("Negotiate ", response, NULL);
600 g_free (response);
601 maj_stat = gss_release_buffer (&min_stat, &out);
602 }
603
604 out:
605 if (out.value)
606 gss_release_buffer (&min_stat, &out);
607 if (in.value)
608 g_free (in.value);
609 return ret;
610 }
611
612 static void
soup_gss_client_cleanup(SoupNegotiateConnectionState * conn)613 soup_gss_client_cleanup (SoupNegotiateConnectionState *conn)
614 {
615 OM_uint32 maj_stat, min_stat;
616
617 gss_release_name (&min_stat, &conn->server_name);
618 maj_stat = gss_delete_sec_context (&min_stat, &conn->context, GSS_C_NO_BUFFER);
619 if (maj_stat != GSS_S_COMPLETE)
620 maj_stat = gss_delete_sec_context (&min_stat, &conn->context, GSS_C_NO_BUFFER);
621 }
622 #endif /* LIBSOUP_HAVE_GSSAPI */
623