/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2007 Red Hat, Inc.
*/
/* This doesn't implement full server-side NTLM, and it mostly doesn't
* even test that the client is doing the crypto/encoding/etc parts of
* NTLM correctly. It only tests that the right message headers get
* set in the right messages.
*/
#include "test-utils.h"
typedef enum {
NTLM_UNAUTHENTICATED,
NTLM_RECEIVED_REQUEST,
NTLM_SENT_CHALLENGE,
NTLM_AUTHENTICATED_ALICE,
NTLM_AUTHENTICATED_BOB
} NTLMServerState;
static const char *state_name[] = {
"unauth", "recv", "sent", "alice", "bob"
};
#define NTLM_REQUEST_START "TlRMTVNTUAABAAAA"
#define NTLM_RESPONSE_START "TlRMTVNTUAADAAAA"
#define NTLM_CHALLENGE "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="
#define NTLMSSP_CHALLENGE "TlRMTVNTUAACAAAADAAMADAAAAABAokAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="
#define NTLM_RESPONSE_USER(response) ((response)[86] == 'E' ? NTLM_AUTHENTICATED_ALICE : ((response)[86] == 'I' ? NTLM_AUTHENTICATED_BOB : NTLM_UNAUTHENTICATED))
typedef struct {
SoupServer *server;
GHashTable *connections;
SoupURI *uri;
gboolean ntlmssp;
} TestServer;
static void
clear_state (gpointer connections, GObject *ex_connection)
{
g_hash_table_remove (connections, ex_connection);
}
static void
server_callback (SoupServer *server, SoupMessage *msg,
const char *path, GHashTable *query,
SoupClientContext *client, gpointer data)
{
TestServer *ts = data;
GSocket *socket;
const char *auth;
NTLMServerState state, required_user = 0;
gboolean auth_required, not_found = FALSE;
gboolean basic_allowed = TRUE, ntlm_allowed = TRUE;
if (msg->method != SOUP_METHOD_GET) {
soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
return;
}
if (!strncmp (path, "/alice", 6))
required_user = NTLM_AUTHENTICATED_ALICE;
else if (!strncmp (path, "/bob", 4))
required_user = NTLM_AUTHENTICATED_BOB;
else if (!strncmp (path, "/either", 7))
;
else if (!strncmp (path, "/basic", 6))
ntlm_allowed = FALSE;
else if (!strncmp (path, "/noauth", 7))
basic_allowed = ntlm_allowed = FALSE;
auth_required = ntlm_allowed || basic_allowed;
if (strstr (path, "/404"))
not_found = TRUE;
socket = soup_client_context_get_gsocket (client);
state = GPOINTER_TO_INT (g_hash_table_lookup (ts->connections, socket));
auth = soup_message_headers_get_one (msg->request_headers,
"Authorization");
if (auth) {
if (!strncmp (auth, "NTLM ", 5)) {
if (!strncmp (auth + 5, NTLM_REQUEST_START,
strlen (NTLM_REQUEST_START))) {
state = NTLM_RECEIVED_REQUEST;
/* If they start, they must finish, even if
* it was unnecessary.
*/
auth_required = ntlm_allowed = TRUE;
basic_allowed = FALSE;
} else if (state == NTLM_SENT_CHALLENGE &&
!strncmp (auth + 5, NTLM_RESPONSE_START,
strlen (NTLM_RESPONSE_START))) {
state = NTLM_RESPONSE_USER (auth + 5);
} else
state = NTLM_UNAUTHENTICATED;
} else if (basic_allowed && !strncmp (auth, "Basic ", 6)) {
gsize len;
char *decoded = (char *)g_base64_decode (auth + 6, &len);
if (!strncmp (decoded, "alice:password", len) &&
required_user != NTLM_AUTHENTICATED_BOB)
auth_required = FALSE;
else if (!strncmp (decoded, "bob:password", len) &&
required_user != NTLM_AUTHENTICATED_ALICE)
auth_required = FALSE;
g_free (decoded);
}
}
if (ntlm_allowed && state > NTLM_SENT_CHALLENGE &&
(!required_user || required_user == state))
auth_required = FALSE;
if (auth_required) {
soup_message_set_status (msg, SOUP_STATUS_UNAUTHORIZED);
if (basic_allowed && state != NTLM_RECEIVED_REQUEST) {
soup_message_headers_append (msg->response_headers,
"WWW-Authenticate",
"Basic realm=\"ntlm-test\"");
}
if (ntlm_allowed && state == NTLM_RECEIVED_REQUEST) {
soup_message_headers_append (msg->response_headers,
"WWW-Authenticate",
ts->ntlmssp ? ("NTLM " NTLMSSP_CHALLENGE) : ("NTLM " NTLM_CHALLENGE));
state = NTLM_SENT_CHALLENGE;
} else if (ntlm_allowed) {
soup_message_headers_append (msg->response_headers,
"WWW-Authenticate", "NTLM");
soup_message_headers_append (msg->response_headers,
"Connection", "close");
}
} else {
if (not_found)
soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
else {
soup_message_set_response (msg, "text/plain",
SOUP_MEMORY_STATIC,
"OK\r\n", 4);
soup_message_set_status (msg, SOUP_STATUS_OK);
}
}
debug_printf (2, " (S:%s)", state_name[state]);
g_hash_table_insert (ts->connections, socket, GINT_TO_POINTER (state));
g_object_weak_ref (G_OBJECT (socket), clear_state, ts->connections);
}
static void
setup_server (TestServer *ts,
gconstpointer test_data)
{
ts->server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
ts->connections = g_hash_table_new (NULL, NULL);
ts->ntlmssp = FALSE;
soup_server_add_handler (ts->server, NULL, server_callback, ts, NULL);
ts->uri = soup_test_server_get_uri (ts->server, "http", NULL);
}
static void
setup_ntlmssp_server (TestServer *ts,
gconstpointer test_data)
{
setup_server (ts, test_data);
ts->ntlmssp = TRUE;
}
static void
teardown_server (TestServer *ts,
gconstpointer test_data)
{
soup_uri_free (ts->uri);
soup_test_server_quit_unref (ts->server);
g_hash_table_destroy (ts->connections);
}
static gboolean authenticated_ntlm = FALSE;
static void
authenticate (SoupSession *session, SoupMessage *msg,
SoupAuth *auth, gboolean retrying, gpointer user)
{
if (!retrying) {
soup_auth_authenticate (auth, user, "password");
if (g_str_equal (soup_auth_get_scheme_name (auth), "NTLM"))
authenticated_ntlm = TRUE;
}
}
typedef struct {
gboolean got_ntlm_prompt;
gboolean got_basic_prompt;
gboolean sent_ntlm_request;
gboolean got_ntlm_challenge;
gboolean sent_ntlm_response;
gboolean sent_basic_response;
} NTLMState;
static void
prompt_check (SoupMessage *msg, gpointer user_data)
{
NTLMState *state = user_data;
const char *header;
header = soup_message_headers_get_list (msg->response_headers,
"WWW-Authenticate");
if (header && strstr (header, "Basic "))
state->got_basic_prompt = TRUE;
if (header && strstr (header, "NTLM") &&
(!strstr (header, NTLM_CHALLENGE) &&
!strstr (header, NTLMSSP_CHALLENGE))) {
state->got_ntlm_prompt = TRUE;
}
}
static void
challenge_check (SoupMessage *msg, gpointer user_data)
{
NTLMState *state = user_data;
const char *header;
header = soup_message_headers_get_list (msg->response_headers,
"WWW-Authenticate");
if (header && !strncmp (header, "NTLM ", 5))
state->got_ntlm_challenge = TRUE;
}
static void
request_check (SoupMessage *msg, gpointer user_data)
{
NTLMState *state = user_data;
const char *header;
header = soup_message_headers_get_one (msg->request_headers,
"Authorization");
if (header && !strncmp (header, "NTLM " NTLM_REQUEST_START,
strlen ("NTLM " NTLM_REQUEST_START)))
state->sent_ntlm_request = TRUE;
}
static void
response_check (SoupMessage *msg, gpointer user_data)
{
NTLMState *state = user_data;
const char *header;
header = soup_message_headers_get_one (msg->request_headers,
"Authorization");
if (header && !strncmp (header, "NTLM " NTLM_RESPONSE_START,
strlen ("NTLM " NTLM_RESPONSE_START)))
state->sent_ntlm_response = TRUE;
if (header && !strncmp (header, "Basic ", 6))
state->sent_basic_response = TRUE;
}
static void
do_message (SoupSession *session, SoupURI *base_uri, const char *path,
gboolean get_ntlm_prompt, gboolean do_ntlm,
gboolean get_basic_prompt, gboolean do_basic,
guint status_code)
{
SoupURI *uri;
SoupMessage *msg;
NTLMState state = { FALSE, FALSE, FALSE, FALSE };
uri = soup_uri_new_with_base (base_uri, path);
msg = soup_message_new_from_uri ("GET", uri);
soup_uri_free (uri);
g_signal_connect (msg, "got_headers",
G_CALLBACK (prompt_check), &state);
g_signal_connect (msg, "got_headers",
G_CALLBACK (challenge_check), &state);
g_signal_connect (msg, "wrote-headers",
G_CALLBACK (request_check), &state);
g_signal_connect (msg, "wrote-headers",
G_CALLBACK (response_check), &state);
soup_session_send_message (session, msg);
debug_printf (1, " %-10s -> ", path);
if (state.got_ntlm_prompt) {
debug_printf (1, " NTLM_PROMPT");
if (!get_ntlm_prompt)
debug_printf (1, "???");
} else if (get_ntlm_prompt)
debug_printf (1, " no-ntlm-prompt???");
if (state.got_basic_prompt) {
debug_printf (1, " BASIC_PROMPT");
if (!get_basic_prompt)
debug_printf (1, "???");
} else if (get_basic_prompt)
debug_printf (1, " no-basic-prompt???");
if (state.sent_ntlm_request) {
debug_printf (1, " REQUEST");
if (!do_ntlm)
debug_printf (1, "???");
} else if (do_ntlm)
debug_printf (1, " no-request???");
if (state.got_ntlm_challenge) {
debug_printf (1, " CHALLENGE");
if (!do_ntlm)
debug_printf (1, "???");
} else if (do_ntlm)
debug_printf (1, " no-challenge???");
if (state.sent_ntlm_response) {
debug_printf (1, " NTLM_RESPONSE");
if (!do_ntlm)
debug_printf (1, "???");
} else if (do_ntlm)
debug_printf (1, " no-ntlm-response???");
if (state.sent_basic_response) {
debug_printf (1, " BASIC_RESPONSE");
if (!do_basic)
debug_printf (1, "???");
} else if (do_basic)
debug_printf (1, " no-basic-response???");
debug_printf (1, " -> %s", msg->reason_phrase);
if (msg->status_code != status_code)
debug_printf (1, "???");
debug_printf (1, "\n");
g_assert_true (state.got_ntlm_prompt == get_ntlm_prompt);
g_assert_true (state.got_basic_prompt == get_basic_prompt);
g_assert_true (state.sent_ntlm_request == do_ntlm);
g_assert_true (state.got_ntlm_challenge == do_ntlm);
g_assert_true (state.sent_ntlm_response == do_ntlm);
g_assert_true (state.sent_basic_response == do_basic);
soup_test_assert_message_status (msg, status_code);
g_object_unref (msg);
}
static void
do_ntlm_round (SoupURI *base_uri, gboolean use_ntlm,
const char *user, gboolean use_builtin_ntlm)
{
SoupSession *session;
gboolean alice = !g_strcmp0 (user, "alice");
gboolean bob = !g_strcmp0 (user, "bob");
gboolean alice_via_ntlm = use_ntlm && alice;
gboolean bob_via_ntlm = use_ntlm && bob;
gboolean alice_via_basic = !use_ntlm && alice;
session = soup_test_session_new (SOUP_TYPE_SESSION, NULL);
if (user) {
g_signal_connect (session, "authenticate",
G_CALLBACK (authenticate), (char *)user);
if (use_ntlm && !use_builtin_ntlm)
g_setenv ("NTLMUSER", user, TRUE);
}
if (use_ntlm) {
SoupAuthManager *auth_manager;
SoupAuth *ntlm;
soup_session_add_feature_by_type (session, SOUP_TYPE_AUTH_NTLM);
auth_manager = SOUP_AUTH_MANAGER (soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER));
ntlm = g_object_new (SOUP_TYPE_AUTH_NTLM, NULL);
soup_auth_manager_use_auth (auth_manager, base_uri, ntlm);
g_object_unref (ntlm);
}
/* 1. Server doesn't request auth, so both get_ntlm_prompt and
* get_basic_prompt are both FALSE, and likewise do_basic. But
* if we're using NTLM we'll try that even without the server
* asking.
*/
authenticated_ntlm = FALSE;
do_message (session, base_uri, "/noauth/",
FALSE, use_ntlm,
FALSE, FALSE,
SOUP_STATUS_OK);
soup_test_assert (authenticated_ntlm == (use_ntlm && use_builtin_ntlm),
"%s built-in NTLM support, but authenticate signal %s emitted\n",
use_builtin_ntlm ? "Using" : "Not using",
authenticated_ntlm ? "was" : "wasn't");
/* 2. Server requires auth as Alice, so it will request that
* if we didn't already authenticate the connection to her in
* the previous step. If we authenticated as Bob in the
* previous step, then we'll just immediately get a 401 here.
* So in no case will we see the client try to do_ntlm.
*/
do_message (session, base_uri, "/alice/",
!alice_via_ntlm, FALSE,
!alice_via_ntlm, alice_via_basic,
alice ? SOUP_STATUS_OK :
SOUP_STATUS_UNAUTHORIZED);
/* 3. Server still requires auth as Alice, but this URI
* doesn't exist, so Alice should get a 404, but others still
* get 401. Alice-via-NTLM is still authenticated, and so
* won't get prompts, and Alice-via-Basic knows at this point
* to send auth without it being requested, so also won't get
* prompts. But Bob/nobody will.
*/
do_message (session, base_uri, "/alice/404",
!alice, bob_via_ntlm,
!alice, alice_via_basic,
alice ? SOUP_STATUS_NOT_FOUND :
SOUP_STATUS_UNAUTHORIZED);
/* 4. Should be exactly the same as #3, except the status code */
do_message (session, base_uri, "/alice/",
!alice, bob_via_ntlm,
!alice, alice_via_basic,
alice ? SOUP_STATUS_OK :
SOUP_STATUS_UNAUTHORIZED);
/* 5. This path requires auth as Bob; Alice-via-NTLM will get
* an immediate 401 and not try to reauthenticate.
* Alice-via-Basic will get a 401 and then try to do Basic
* (and fail). Bob-via-NTLM will try to do NTLM right away and
* succeed.
*/
do_message (session, base_uri, "/bob/",
!bob_via_ntlm, bob_via_ntlm,
!bob_via_ntlm, alice_via_basic,
bob ? SOUP_STATUS_OK :
SOUP_STATUS_UNAUTHORIZED);
/* 6. Back to /alice. Somewhat the inverse of #5; Bob-via-NTLM
* will get an immediate 401 and not try again, Alice-via-NTLM
* will try to do NTLM right away and succeed. Alice-via-Basic
* still knows about this path, so will try Basic right away
* and succeed.
*/
do_message (session, base_uri, "/alice/",
!alice_via_ntlm, alice_via_ntlm,
!alice_via_ntlm, alice_via_basic,
alice ? SOUP_STATUS_OK :
SOUP_STATUS_UNAUTHORIZED);
/* 7. Server accepts Basic auth from either user, but not NTLM.
* Since Bob-via-NTLM is unauthenticated at this point, he'll try
* NTLM before realizing that the server doesn't support it.
*/
do_message (session, base_uri, "/basic/",
FALSE, bob_via_ntlm,
TRUE, user != NULL,
user != NULL ? SOUP_STATUS_OK :
SOUP_STATUS_UNAUTHORIZED);
/* 8. Server accepts Basic or NTLM from either user.
* NTLM users will try NTLM without getting a prompt (their
* previous NTLM connections will have been closed by the 401
* from /basic). Non-NTLM users will be prompted for either.
*/
do_message (session, base_uri, "/either/",
!use_ntlm, use_ntlm,
!use_ntlm, !use_ntlm && user != NULL,
user != NULL ? SOUP_STATUS_OK :
SOUP_STATUS_UNAUTHORIZED);
soup_test_session_abort_unref (session);
}
typedef enum {
BUILTIN,
WINBIND,
FALLBACK
} NtlmType;
typedef struct {
const char *name, *user;
gboolean conn_uses_ntlm;
NtlmType ntlm_type;
} NtlmTest;
static const NtlmTest ntlm_tests[] = {
{ "/ntlm/builtin/none", NULL, FALSE, BUILTIN },
{ "/ntlm/builtin/alice", "alice", TRUE, BUILTIN },
{ "/ntlm/builtin/bob", "bob", TRUE, BUILTIN },
{ "/ntlm/builtin/basic", "alice", FALSE, BUILTIN },
{ "/ntlm/winbind/none", NULL, FALSE, WINBIND },
{ "/ntlm/winbind/alice", "alice", TRUE, WINBIND },
{ "/ntlm/winbind/bob", "bob", TRUE, WINBIND },
{ "/ntlm/winbind/basic", "alice", FALSE, WINBIND },
{ "/ntlm/fallback/none", NULL, FALSE, FALLBACK },
{ "/ntlm/fallback/alice", "alice", TRUE, FALLBACK },
{ "/ntlm/fallback/bob", "bob", TRUE, FALLBACK },
{ "/ntlm/fallback/basic", "alice", FALSE, FALLBACK }
};
static const NtlmTest ntlmssp_tests[] = {
{ "/ntlm/ssp/none", NULL, FALSE, BUILTIN },
{ "/ntlm/ssp/alice", "alice", TRUE, BUILTIN },
{ "/ntlm/ssp/bob", "bob", TRUE, BUILTIN },
{ "/ntlm/ssp/basic", "alice", FALSE, BUILTIN }
};
static void
do_ntlm_test (TestServer *ts,
gconstpointer data)
{
const NtlmTest *test = data;
gboolean use_builtin_ntlm = TRUE;
switch (test->ntlm_type) {
case BUILTIN:
/* Built-in NTLM auth support. (We set SOUP_NTLM_AUTH_DEBUG to
* an empty string to ensure that the built-in support is
* being used, even if /usr/bin/ntlm_auth is available.)
*/
g_setenv ("SOUP_NTLM_AUTH_DEBUG", "", TRUE);
break;
case WINBIND:
#ifndef USE_NTLM_AUTH
g_test_skip ("/usr/bin/ntlm_auth is not available");
return;
#endif
/* Samba winbind /usr/bin/ntlm_auth helper support (via a
* helper program that emulates its interface).
*/
g_setenv ("SOUP_NTLM_AUTH_DEBUG",
g_test_get_filename (G_TEST_BUILT, "ntlm-test-helper", NULL),
TRUE);
g_unsetenv ("SOUP_NTLM_AUTH_DEBUG_NOCREDS");
use_builtin_ntlm = FALSE;
break;
case FALLBACK:
#ifndef USE_NTLM_AUTH
g_test_skip ("/usr/bin/ntlm_auth is not available");
return;
#endif
/* Support for when ntlm_auth is installed, but the user has
* no cached credentials (and thus we have to fall back to
* libsoup's built-in NTLM support).
*/
g_setenv ("SOUP_NTLM_AUTH_DEBUG",
g_test_get_filename (G_TEST_BUILT, "ntlm-test-helper", NULL),
TRUE);
g_setenv ("SOUP_NTLM_AUTH_DEBUG_NOCREDS", "1", TRUE);
break;
}
do_ntlm_round (ts->uri, test->conn_uses_ntlm, test->user, use_builtin_ntlm);
}
static void
retry_test_authenticate (SoupSession *session, SoupMessage *msg,
SoupAuth *auth, gboolean retrying,
gpointer user_data)
{
gboolean *retried = user_data;
if (!retrying) {
/* server_callback doesn't actually verify the password,
* only the username. So we pass an incorrect username
* rather than an incorrect password.
*/
soup_auth_authenticate (auth, "wrong", "password");
} else if (!*retried) {
soup_auth_authenticate (auth, "alice", "password");
*retried = TRUE;
}
}
static void
do_retrying_test (TestServer *ts,
gconstpointer data)
{
SoupSession *session;
SoupMessage *msg;
SoupURI *uri;
gboolean retried = FALSE;
g_test_bug ("693222");
g_setenv ("SOUP_NTLM_AUTH_DEBUG", "", TRUE);
debug_printf (1, " /alice\n");
session = soup_test_session_new (SOUP_TYPE_SESSION,
SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_AUTH_NTLM,
NULL);
g_signal_connect (session, "authenticate",
G_CALLBACK (retry_test_authenticate), &retried);
uri = soup_uri_new_with_base (ts->uri, "/alice");
msg = soup_message_new_from_uri ("GET", uri);
soup_uri_free (uri);
soup_session_send_message (session, msg);
g_assert_true (retried);
soup_test_assert_message_status (msg, SOUP_STATUS_OK);
g_object_unref (msg);
soup_test_session_abort_unref (session);
debug_printf (1, " /bob\n");
session = soup_test_session_new (SOUP_TYPE_SESSION,
SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_AUTH_NTLM,
NULL);
g_signal_connect (session, "authenticate",
G_CALLBACK (retry_test_authenticate), &retried);
retried = FALSE;
uri = soup_uri_new_with_base (ts->uri, "/bob");
msg = soup_message_new_from_uri ("GET", uri);
soup_uri_free (uri);
soup_session_send_message (session, msg);
g_assert_true (retried);
soup_test_assert_message_status (msg, SOUP_STATUS_UNAUTHORIZED);
g_object_unref (msg);
soup_test_session_abort_unref (session);
}
int
main (int argc, char **argv)
{
int i, ret;
test_init (argc, argv, NULL);
for (i = 0; i < G_N_ELEMENTS (ntlm_tests); i++) {
g_test_add (ntlm_tests[i].name, TestServer, &ntlm_tests[i],
setup_server, do_ntlm_test, teardown_server);
}
for (i = 0; i < G_N_ELEMENTS (ntlmssp_tests); i++) {
g_test_add (ntlmssp_tests[i].name, TestServer, &ntlmssp_tests[i],
setup_ntlmssp_server, do_ntlm_test, teardown_server);
}
g_test_add ("/ntlm/retry", TestServer, NULL,
setup_server, do_retrying_test, teardown_server);
ret = g_test_run ();
test_cleanup ();
return ret;
}