Fix the retry-on-broken-connection codepath for SoupRequest
[platform/upstream/libsoup.git] / tests / ntlm-test.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2007 Red Hat, Inc.
4  */
5
6 /* This doesn't implement full server-side NTLM, and it mostly doesn't
7  * even test that the client is doing the crypto/encoding/etc parts of
8  * NTLM correctly. It only tests that the right message headers get
9  * set in the right messages.
10  */
11
12 #include "test-utils.h"
13
14 typedef enum {
15         NTLM_UNAUTHENTICATED,
16         NTLM_RECEIVED_REQUEST,
17         NTLM_SENT_CHALLENGE,
18         NTLM_AUTHENTICATED_ALICE,
19         NTLM_AUTHENTICATED_BOB
20 } NTLMServerState;
21
22 #define NTLM_REQUEST_START "TlRMTVNTUAABAAAA"
23 #define NTLM_RESPONSE_START "TlRMTVNTUAADAAAA"
24
25 #define NTLM_CHALLENGE "TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="
26
27 #define NTLM_RESPONSE_USER(response) ((response)[102] == 'E' ? NTLM_AUTHENTICATED_ALICE : NTLM_AUTHENTICATED_BOB)
28
29 static void
30 clear_state (gpointer connections, GObject *ex_connection)
31 {
32         g_hash_table_remove (connections, ex_connection);
33 }
34
35 static void
36 server_callback (SoupServer *server, SoupMessage *msg,
37                  const char *path, GHashTable *query,
38                  SoupClientContext *client, gpointer data)
39 {
40         GHashTable *connections = data;
41         SoupSocket *socket;
42         const char *auth;
43         NTLMServerState state, required_user = 0;
44         gboolean auth_required, not_found = FALSE;
45         gboolean basic_allowed = TRUE, ntlm_allowed = TRUE;
46
47         if (msg->method != SOUP_METHOD_GET) {
48                 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
49                 return;
50         }
51
52         if (!strncmp (path, "/alice", 6))
53                 required_user = NTLM_AUTHENTICATED_ALICE;
54         else if (!strncmp (path, "/bob", 4))
55                 required_user = NTLM_AUTHENTICATED_BOB;
56         else if (!strncmp (path, "/either", 7))
57                 ;
58         else if (!strncmp (path, "/basic", 6))
59                 ntlm_allowed = FALSE;
60         else if (!strncmp (path, "/noauth", 7))
61                 basic_allowed = ntlm_allowed = FALSE;
62         auth_required = ntlm_allowed || basic_allowed;
63
64         if (strstr (path, "/404"))
65                 not_found = TRUE;
66
67         socket = soup_client_context_get_socket (client);
68         state = GPOINTER_TO_INT (g_hash_table_lookup (connections, socket));
69         auth = soup_message_headers_get_one (msg->request_headers,
70                                              "Authorization");
71
72         if (auth) {
73                 if (!strncmp (auth, "NTLM ", 5)) {
74                         if (!strncmp (auth + 5, NTLM_REQUEST_START,
75                                       strlen (NTLM_REQUEST_START))) {
76                                 state = NTLM_RECEIVED_REQUEST;
77                                 /* If they start, they must finish, even if
78                                  * it was unnecessary.
79                                  */
80                                 auth_required = ntlm_allowed = TRUE;
81                                 basic_allowed = FALSE;
82                         } else if (state == NTLM_SENT_CHALLENGE &&
83                                    !strncmp (auth + 5, NTLM_RESPONSE_START,
84                                              strlen (NTLM_RESPONSE_START))) {
85                                 state = NTLM_RESPONSE_USER (auth + 5);
86                         } else
87                                 state = NTLM_UNAUTHENTICATED;
88                 } else if (basic_allowed && !strncmp (auth, "Basic ", 6)) {
89                         gsize len;
90                         char *decoded = (char *)g_base64_decode (auth + 6, &len);
91
92                         if (!strncmp (decoded, "alice:password", len) &&
93                             required_user != NTLM_AUTHENTICATED_BOB)
94                                 auth_required = FALSE;
95                         else if (!strncmp (decoded, "bob:password", len) &&
96                                  required_user != NTLM_AUTHENTICATED_ALICE)
97                                 auth_required = FALSE;
98                         g_free (decoded);
99                 }
100         }
101
102         if (ntlm_allowed && state > NTLM_SENT_CHALLENGE &&
103             (!required_user || required_user == state))
104                 auth_required = FALSE;
105
106         if (auth_required) {
107                 soup_message_set_status (msg, SOUP_STATUS_UNAUTHORIZED);
108
109                 if (basic_allowed && state != NTLM_RECEIVED_REQUEST) {
110                         soup_message_headers_append (msg->response_headers,
111                                                      "WWW-Authenticate",
112                                                      "Basic realm=\"ntlm-test\"");
113                 }
114
115                 if (ntlm_allowed && state == NTLM_RECEIVED_REQUEST) {
116                         soup_message_headers_append (msg->response_headers,
117                                                      "WWW-Authenticate",
118                                                      "NTLM " NTLM_CHALLENGE);
119                         state = NTLM_SENT_CHALLENGE;
120                 } else if (ntlm_allowed) {
121                         soup_message_headers_append (msg->response_headers,
122                                                      "WWW-Authenticate", "NTLM");
123                         soup_message_headers_append (msg->response_headers,
124                                                      "Connection", "close");
125                 }
126         } else {
127                 if (not_found)
128                         soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
129                 else {
130                         soup_message_set_response (msg, "text/plain",
131                                                    SOUP_MEMORY_STATIC,
132                                                    "OK\r\n", 4);
133                         soup_message_set_status (msg, SOUP_STATUS_OK);
134                 }
135         }
136
137         g_hash_table_insert (connections, socket, GINT_TO_POINTER (state));
138         g_object_weak_ref (G_OBJECT (socket), clear_state, connections);
139 }
140
141 static void
142 authenticate (SoupSession *session, SoupMessage *msg,
143               SoupAuth *auth, gboolean retrying, gpointer user)
144 {
145         if (!retrying)
146                 soup_auth_authenticate (auth, user, "password");
147 }
148
149 typedef struct {
150         gboolean got_ntlm_prompt;
151         gboolean got_basic_prompt;
152         gboolean sent_ntlm_request;
153         gboolean got_ntlm_challenge;
154         gboolean sent_ntlm_response;
155         gboolean sent_basic_response;
156 } NTLMState;
157
158 static void
159 prompt_check (SoupMessage *msg, gpointer user_data)
160 {
161         NTLMState *state = user_data;
162         const char *header;
163
164         header = soup_message_headers_get_list (msg->response_headers,
165                                                 "WWW-Authenticate");
166         if (header && strstr (header, "Basic "))
167                 state->got_basic_prompt = TRUE;
168         if (header && strstr (header, "NTLM") &&
169             !strstr (header, NTLM_CHALLENGE))
170                 state->got_ntlm_prompt = TRUE;
171 }
172
173 static void
174 challenge_check (SoupMessage *msg, gpointer user_data)
175 {
176         NTLMState *state = user_data;
177         const char *header;
178
179         header = soup_message_headers_get_list (msg->response_headers,
180                                                 "WWW-Authenticate");
181         if (header && !strncmp (header, "NTLM ", 5))
182                 state->got_ntlm_challenge = TRUE;
183 }
184
185 static void
186 request_check (SoupMessage *msg, gpointer user_data)
187 {
188         NTLMState *state = user_data;
189         const char *header;
190
191         header = soup_message_headers_get_one (msg->request_headers,
192                                                "Authorization");
193         if (header && !strncmp (header, "NTLM " NTLM_REQUEST_START,
194                                 strlen ("NTLM " NTLM_REQUEST_START)))
195                 state->sent_ntlm_request = TRUE;
196 }
197
198 static void
199 response_check (SoupMessage *msg, gpointer user_data)
200 {
201         NTLMState *state = user_data;
202         const char *header;
203
204         header = soup_message_headers_get_one (msg->request_headers,
205                                                "Authorization");
206         if (header && !strncmp (header, "NTLM " NTLM_RESPONSE_START,
207                                 strlen ("NTLM " NTLM_RESPONSE_START)))
208                 state->sent_ntlm_response = TRUE;
209         if (header && !strncmp (header, "Basic ", 6))
210                 state->sent_basic_response = TRUE;
211 }
212
213 static void
214 do_message (SoupSession *session, SoupURI *base_uri, const char *path,
215             gboolean get_ntlm_prompt, gboolean do_ntlm,
216             gboolean get_basic_prompt, gboolean do_basic,
217             guint status_code)
218 {
219         SoupURI *uri;
220         SoupMessage *msg;
221         NTLMState state = { FALSE, FALSE, FALSE, FALSE };
222
223         uri = soup_uri_new_with_base (base_uri, path);
224         msg = soup_message_new_from_uri ("GET", uri);
225         soup_uri_free (uri);
226
227         g_signal_connect (msg, "got_headers",
228                           G_CALLBACK (prompt_check), &state);
229         g_signal_connect (msg, "got_headers",
230                           G_CALLBACK (challenge_check), &state);
231         g_signal_connect (msg, "wrote-headers",
232                           G_CALLBACK (request_check), &state);
233         g_signal_connect (msg, "wrote-headers",
234                           G_CALLBACK (response_check), &state);
235
236         soup_session_send_message (session, msg);
237         debug_printf (1, "  %-10s -> ", path);
238
239         if (state.got_ntlm_prompt) {
240                 debug_printf (1, " NTLM_PROMPT");
241                 if (!get_ntlm_prompt) {
242                         debug_printf (1, "???");
243                         errors++;
244                 }
245         } else if (get_ntlm_prompt) {
246                 debug_printf (1, " no-ntlm-prompt???");
247                 errors++;
248         }
249
250         if (state.got_basic_prompt) {
251                 debug_printf (1, " BASIC_PROMPT");
252                 if (!get_basic_prompt) {
253                         debug_printf (1, "???");
254                         errors++;
255                 }
256         } else if (get_basic_prompt) {
257                 debug_printf (1, " no-basic-prompt???");
258                 errors++;
259         }
260
261         if (state.sent_ntlm_request) {
262                 debug_printf (1, " REQUEST");
263                 if (!do_ntlm) {
264                         debug_printf (1, "???");
265                         errors++;
266                 }
267         } else if (do_ntlm) {
268                 debug_printf (1, " no-request???");
269                 errors++;
270         }
271
272         if (state.got_ntlm_challenge) {
273                 debug_printf (1, " CHALLENGE");
274                 if (!do_ntlm) {
275                         debug_printf (1, "???");
276                         errors++;
277                 }
278         } else if (do_ntlm) {
279                 debug_printf (1, " no-challenge???");
280                 errors++;
281         }
282
283         if (state.sent_ntlm_response) {
284                 debug_printf (1, " NTLM_RESPONSE");
285                 if (!do_ntlm) {
286                         debug_printf (1, "???");
287                         errors++;
288                 }
289         } else if (do_ntlm) {
290                 debug_printf (1, " no-ntlm-response???");
291                 errors++;
292         }
293
294         if (state.sent_basic_response) {
295                 debug_printf (1, " BASIC_RESPONSE");
296                 if (!do_basic) {
297                         debug_printf (1, "???");
298                         errors++;
299                 }
300         } else if (do_basic) {
301                 debug_printf (1, " no-basic-response???");
302                 errors++;
303         }
304
305         debug_printf (1, " -> %s", msg->reason_phrase);
306         if (msg->status_code != status_code) {
307                 debug_printf (1, "???");
308                 errors++;
309         }
310         debug_printf (1, "\n");
311
312         g_object_unref (msg);
313 }
314
315 static void
316 do_ntlm_round (SoupURI *base_uri, gboolean use_ntlm, const char *user)
317 {
318         SoupSession *session;
319         gboolean alice = !g_strcmp0 (user, "alice");
320         gboolean bob = !g_strcmp0 (user, "bob");
321         gboolean alice_via_ntlm = use_ntlm && alice;
322         gboolean bob_via_ntlm = use_ntlm && bob;
323         gboolean alice_via_basic = !use_ntlm && alice;
324
325         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
326         if (use_ntlm)
327                 soup_session_add_feature_by_type (session, SOUP_TYPE_AUTH_NTLM);
328
329         if (user) {
330                 g_signal_connect (session, "authenticate",
331                                   G_CALLBACK (authenticate), (char *)user);
332         }
333
334         /* 1. Server doesn't request auth, so both get_ntlm_prompt and
335          * get_basic_prompt are both FALSE, and likewise do_basic. But
336          * if we're using NTLM we'll try that even without the server
337          * asking.
338          */
339         do_message (session, base_uri, "/noauth",
340                     FALSE, use_ntlm,
341                     FALSE, FALSE,
342                     SOUP_STATUS_OK);
343
344         /* 2. Server requires auth as Alice, so it will request that
345          * if we didn't already authenticate the connection to her in
346          * the previous step. If we authenticated as Bob in the
347          * previous step, then we'll just immediately get a 401 here.
348          * So in no case will we see the client try to do_ntlm.
349          */
350         do_message (session, base_uri, "/alice",
351                     !alice_via_ntlm, FALSE,
352                     !alice_via_ntlm, alice_via_basic,
353                     alice ? SOUP_STATUS_OK :
354                     SOUP_STATUS_UNAUTHORIZED);
355
356         /* 3. Server still requires auth as Alice, but this URI
357          * doesn't exist, so Alice should get a 404, but others still
358          * get 401. Alice-via-NTLM is still authenticated, and so
359          * won't get prompts, and Alice-via-Basic knows at this point
360          * to send auth without it being requested, so also won't get
361          * prompts. But Bob/nobody will.
362          */
363         do_message (session, base_uri, "/alice/404",
364                     !alice, bob_via_ntlm,
365                     !alice, alice_via_basic,
366                     alice ? SOUP_STATUS_NOT_FOUND :
367                     SOUP_STATUS_UNAUTHORIZED);
368
369         /* 4. Should be exactly the same as #3, except the status code */
370         do_message (session, base_uri, "/alice",
371                     !alice, bob_via_ntlm,
372                     !alice, alice_via_basic,
373                     alice ? SOUP_STATUS_OK :
374                     SOUP_STATUS_UNAUTHORIZED);
375
376         /* 5. This path requires auth as Bob; Alice-via-NTLM will get
377          * an immediate 401 and not try to reauthenticate.
378          * Alice-via-Basic will get a 401 and then try to do Basic
379          * (and fail). Bob-via-NTLM will try to do NTLM right away and
380          * succeed.
381          */
382         do_message (session, base_uri, "/bob",
383                     !bob_via_ntlm, bob_via_ntlm,
384                     !bob_via_ntlm, alice_via_basic,
385                     bob ? SOUP_STATUS_OK :
386                     SOUP_STATUS_UNAUTHORIZED);
387
388         /* 6. Back to /alice. Somewhat the inverse of #5; Bob-via-NTLM
389          * will get an immediate 401 and not try again, Alice-via-NTLM
390          * will try to do NTLM right away and succeed. Alice-via-Basic
391          * still knows about this path, so will try Basic right away
392          * and succeed.
393          */
394         do_message (session, base_uri, "/alice",
395                     !alice_via_ntlm, alice_via_ntlm,
396                     !alice_via_ntlm, alice_via_basic,
397                     alice ? SOUP_STATUS_OK :
398                     SOUP_STATUS_UNAUTHORIZED);
399
400         /* 7. Server accepts Basic auth from either user, but not NTLM.
401          * Since Bob-via-NTLM is unauthenticated at this point, he'll try
402          * NTLM before realizing that the server doesn't support it.
403          */
404         do_message (session, base_uri, "/basic",
405                     FALSE, bob_via_ntlm,
406                     TRUE, user != NULL,
407                     user != NULL ? SOUP_STATUS_OK :
408                     SOUP_STATUS_UNAUTHORIZED);
409
410         /* 8. Server accepts Basic or NTLM from either user.
411          * Alice-via-NTLM is still authenticated at this point from #6,
412          * and Bob-via-NTLM is authenticated from #7, so neither
413          * of them will do anything.
414          */
415         do_message (session, base_uri, "/either",
416                     !use_ntlm, FALSE,
417                     !use_ntlm, !use_ntlm && user != NULL,
418                     user != NULL ? SOUP_STATUS_OK :
419                     SOUP_STATUS_UNAUTHORIZED);
420
421         soup_test_session_abort_unref (session);
422 }
423
424 static void
425 do_ntlm_tests (SoupURI *base_uri)
426 {
427         debug_printf (1, "Round 1: Non-NTLM Connection, no auth\n");
428         do_ntlm_round (base_uri, FALSE, NULL);
429         debug_printf (1, "Round 2: NTLM Connection, user=alice\n");
430         do_ntlm_round (base_uri, TRUE, "alice");
431         debug_printf (1, "Round 3: NTLM Connection, user=bob\n");
432         do_ntlm_round (base_uri, TRUE, "bob");
433         debug_printf (1, "Round 4: Non-NTLM Connection, user=alice\n");
434         do_ntlm_round (base_uri, FALSE, "alice");
435 }
436
437 int
438 main (int argc, char **argv)
439 {
440         GMainLoop *loop;
441         SoupServer *server;
442         GHashTable *connections;
443         SoupURI *uri;
444
445         test_init (argc, argv, NULL);
446
447         server = soup_test_server_new (FALSE);
448         connections = g_hash_table_new (NULL, NULL);
449         soup_server_add_handler (server, NULL,
450                                  server_callback, connections, NULL);
451
452         loop = g_main_loop_new (NULL, TRUE);
453
454         uri = soup_uri_new ("http://127.0.0.1/");
455         soup_uri_set_port (uri, soup_server_get_port (server));
456         do_ntlm_tests (uri);
457         soup_uri_free (uri);
458
459         g_main_loop_unref (loop);
460
461         soup_test_server_quit_unref (server);
462         test_cleanup ();
463         g_hash_table_destroy (connections);
464
465         return errors != 0;
466 }