soup-message-io: keep cancellable alive for duration of io_run_until
[platform/upstream/libsoup.git] / tests / redirect-test.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2008 Red Hat, Inc.
4  */
5
6 #include "config.h"
7
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11
12 #include <glib.h>
13
14 #define LIBSOUP_USE_UNSTABLE_REQUEST_API
15 #include <libsoup/soup.h>
16 #include <libsoup/soup-requester.h>
17 #include <libsoup/soup-request-http.h>
18
19 #include "test-utils.h"
20
21 char *server2_uri;
22
23 typedef struct {
24         const char *method;
25         const char *path;
26         guint status_code;
27         gboolean repeat;
28 } TestRequest;
29
30 static struct {
31         TestRequest requests[3];
32         guint final_status;
33         guint request_api_final_status;
34 } tests[] = {
35         /* A redirecty response to a GET or HEAD should cause a redirect */
36
37         { { { "GET", "/301", 301 },
38             { "GET", "/", 200 },
39             { NULL } }, 200 },
40         { { { "GET", "/302", 302 },
41             { "GET", "/", 200 },
42             { NULL } }, 200 },
43         { { { "GET", "/303", 303 },
44             { "GET", "/", 200 },
45             { NULL } }, 200 },
46         { { { "GET", "/307", 307 },
47             { "GET", "/", 200 },
48             { NULL } }, 200 },
49         { { { "HEAD", "/301", 301 },
50             { "HEAD", "/", 200 },
51             { NULL } }, 200 },
52         { { { "HEAD", "/302", 302 },
53             { "HEAD", "/", 200 },
54             { NULL } }, 200 },
55         /* 303 is a nonsensical response to HEAD, but some sites do
56          * it anyway. :-/
57          */
58         { { { "HEAD", "/303", 303 },
59             { "HEAD", "/", 200 },
60             { NULL } }, 200 },
61         { { { "HEAD", "/307", 307 },
62             { "HEAD", "/", 200 },
63             { NULL } }, 200 },
64
65         /* A non-redirecty response to a GET or HEAD should not */
66
67         { { { "GET", "/300", 300 },
68             { NULL } }, 300 },
69         { { { "GET", "/304", 304 },
70             { NULL } }, 304 },
71         { { { "GET", "/305", 305 },
72             { NULL } }, 305 },
73         { { { "GET", "/306", 306 },
74             { NULL } }, 306 },
75         { { { "GET", "/308", 308 },
76             { NULL } }, 308 },
77         { { { "HEAD", "/300", 300 },
78             { NULL } }, 300 },
79         { { { "HEAD", "/304", 304 },
80             { NULL } }, 304 },
81         { { { "HEAD", "/305", 305 },
82             { NULL } }, 305 },
83         { { { "HEAD", "/306", 306 },
84             { NULL } }, 306 },
85         { { { "HEAD", "/308", 308 },
86             { NULL } }, 308 },
87         
88         /* Test double-redirect */
89
90         { { { "GET", "/301/302", 301 },
91             { "GET", "/302", 302 },
92             { "GET", "/", 200 } }, 200 },
93         { { { "HEAD", "/301/302", 301 },
94             { "HEAD", "/302", 302 },
95             { "HEAD", "/", 200 } }, 200 },
96
97         /* POST should only automatically redirect on 301, 302 and 303 */
98
99         { { { "POST", "/301", 301 },
100             { "GET", "/", 200 },
101             { NULL } }, 200 },
102         { { { "POST", "/302", 302 },
103             { "GET", "/", 200 },
104             { NULL } }, 200 },
105         { { { "POST", "/303", 303 },
106             { "GET", "/", 200 },
107             { NULL } }, 200 },
108         { { { "POST", "/307", 307 },
109             { NULL } }, 307 },
110
111         /* Test behavior with recoverably-bad Location header */
112         { { { "GET", "/bad", 302 },
113             { "GET", "/bad%20with%20spaces", 200 },
114             { NULL } }, 200 },
115
116         /* Test behavior with irrecoverably-bad Location header */
117         { { { "GET", "/bad-no-host", 302 },
118             { NULL } }, SOUP_STATUS_MALFORMED, 302 },
119
120         /* Test infinite redirection */
121         { { { "GET", "/bad-recursive", 302, TRUE },
122             { NULL } }, SOUP_STATUS_TOO_MANY_REDIRECTS },
123
124         /* Test redirection to a different server */
125         { { { "GET", "/server2", 302 },
126             { "GET", "/on-server2", 200 },
127             { NULL } }, 200 },
128 };
129 static const int n_tests = G_N_ELEMENTS (tests);
130
131 static void
132 got_headers (SoupMessage *msg, gpointer user_data)
133 {
134         TestRequest **treq = user_data;
135         const char *location;
136
137         debug_printf (2, "    -> %d %s\n", msg->status_code,
138                       msg->reason_phrase);
139         location = soup_message_headers_get_one (msg->response_headers,
140                                                  "Location");
141         if (location)
142                 debug_printf (2, "       Location: %s\n", location);
143
144         if (!(*treq)->method)
145                 return;
146
147         if (msg->status_code != (*treq)->status_code) {
148                 debug_printf (1, "    - Expected %d !\n",
149                               (*treq)->status_code);
150                 errors++;
151         }
152 }
153
154 static void
155 restarted (SoupMessage *msg, gpointer user_data)
156 {
157         TestRequest **treq = user_data;
158         SoupURI *uri = soup_message_get_uri (msg);
159
160         debug_printf (2, "    %s %s\n", msg->method, uri->path);
161
162         if ((*treq)->method && !(*treq)->repeat)
163                 (*treq)++;
164
165         if (!(*treq)->method) {
166                 debug_printf (1, "    - Expected to be done!\n");
167                 errors++;
168                 return;
169         }
170
171         if (strcmp (msg->method, (*treq)->method) != 0) {
172                 debug_printf (1, "    - Expected %s !\n", (*treq)->method);
173                 errors++;
174         }
175         if (strcmp (uri->path, (*treq)->path) != 0) {
176                 debug_printf (1, "    - Expected %s !\n", (*treq)->path);
177                 errors++;
178         }
179 }
180
181 static void
182 do_message_api_test (SoupSession *session, SoupURI *base_uri, int n)
183 {
184         SoupURI *uri;
185         SoupMessage *msg;
186         TestRequest *treq;
187
188         debug_printf (1, "%2d. %s %s\n", n + 1,
189                       tests[n].requests[0].method,
190                       tests[n].requests[0].path);
191
192         uri = soup_uri_new_with_base (base_uri, tests[n].requests[0].path);
193         msg = soup_message_new_from_uri (tests[n].requests[0].method, uri);
194         soup_uri_free (uri);
195
196         if (msg->method == SOUP_METHOD_POST) {
197                 soup_message_set_request (msg, "text/plain",
198                                           SOUP_MEMORY_STATIC,
199                                           "post body",
200                                           strlen ("post body"));
201         }
202
203         treq = &tests[n].requests[0];
204         g_signal_connect (msg, "got_headers",
205                           G_CALLBACK (got_headers), &treq);
206         g_signal_connect (msg, "restarted",
207                           G_CALLBACK (restarted), &treq);
208
209         soup_session_send_message (session, msg);
210
211         if (msg->status_code != tests[n].final_status) {
212                 debug_printf (1, "    - Expected final status of %d, got %d !\n",
213                               tests[n].final_status, msg->status_code);
214                 errors++;
215         }
216
217         g_object_unref (msg);
218         debug_printf (2, "\n");
219 }
220
221 static void
222 async_callback (GObject *source, GAsyncResult *result, gpointer user_data)
223 {
224         GAsyncResult **result_p = user_data;
225
226         *result_p = g_object_ref (result);
227 }
228
229 static void
230 do_request_api_test (SoupSession *session, SoupURI *base_uri, int n)
231 {
232         SoupRequester *requester = (SoupRequester *)soup_session_get_feature (session, SOUP_TYPE_REQUESTER);
233         SoupURI *uri;
234         SoupRequest *req;
235         SoupMessage *msg;
236         TestRequest *treq;
237         GInputStream *stream;
238         GAsyncResult *result;
239         GError *error = NULL;
240         guint final_status;
241
242         debug_printf (1, "%2d. %s %s\n", n + 1,
243                       tests[n].requests[0].method,
244                       tests[n].requests[0].path);
245
246         final_status = tests[n].request_api_final_status;
247         if (!final_status)
248                 final_status = tests[n].final_status;
249
250         uri = soup_uri_new_with_base (base_uri, tests[n].requests[0].path);
251         req = soup_requester_request_uri (requester, uri, &error);
252         soup_uri_free (uri);
253         if (!req) {
254                 debug_printf (1, "    could not create request: %s\n",
255                               error->message);
256                 g_error_free (error);
257                 errors++;
258                 debug_printf (2, "\n");
259                 return;
260         }
261
262         msg = soup_request_http_get_message (SOUP_REQUEST_HTTP (req));
263         g_object_set (G_OBJECT (msg),
264                       SOUP_MESSAGE_METHOD, tests[n].requests[0].method,
265                       NULL);
266
267         if (msg->method == SOUP_METHOD_POST) {
268                 soup_message_set_request (msg, "text/plain",
269                                           SOUP_MEMORY_STATIC,
270                                           "post body",
271                                           strlen ("post body"));
272         }
273
274         treq = &tests[n].requests[0];
275         g_signal_connect (msg, "got_headers",
276                           G_CALLBACK (got_headers), &treq);
277         g_signal_connect (msg, "restarted",
278                           G_CALLBACK (restarted), &treq);
279
280         if (SOUP_IS_SESSION_SYNC (session)) {
281                 stream = soup_request_send (req, NULL, &error);
282         } else {
283                 result = NULL;
284                 soup_request_send_async (req, NULL, async_callback, &result);
285                 while (!result)
286                         g_main_context_iteration (NULL, TRUE);
287                 stream = soup_request_send_finish (req, result, &error);
288                 g_object_unref (result);
289         }
290
291         if (SOUP_STATUS_IS_TRANSPORT_ERROR (final_status)) {
292                 if (stream) {
293                         debug_printf (1, "    expected failure (%s) but succeeded",
294                                       soup_status_get_phrase (final_status));
295                         errors++;
296                         g_object_unref (stream);
297                 }
298                 if (error->domain != SOUP_HTTP_ERROR ||
299                     error->code != final_status) {
300                         debug_printf (1, "    expected '%s' but got '%s'",
301                                       soup_status_get_phrase (final_status),
302                                       error->message);
303                         errors++;
304                 }
305
306                 g_error_free (error);
307                 g_object_unref (req);
308                 debug_printf (2, "\n");
309                 return;
310         } else if (!stream) {
311                 debug_printf (1, "    could not send request: %s\n",
312                               error->message);
313                 g_error_free (error);
314                 g_object_unref (req);
315                 errors++;
316                 debug_printf (2, "\n");
317                 return;
318         }
319
320         if (SOUP_IS_SESSION_SYNC (session)) {
321                 g_input_stream_close (stream, NULL, &error);
322         } else {
323                 result = NULL;
324                 g_input_stream_close_async (stream, G_PRIORITY_DEFAULT,
325                                             NULL, async_callback, &result);
326                 while (!result)
327                         g_main_context_iteration (NULL, TRUE);
328                 g_input_stream_close_finish (stream, result, &error);
329                 g_object_unref (result);
330         }
331         if (error) {
332                 debug_printf (1, "    could not close stream: %s\n",
333                               error->message);
334                 g_error_free (error);
335                 errors++;
336         }
337
338         if (msg->status_code != final_status) {
339                 debug_printf (1, "    - Expected final status of %d, got %d !\n",
340                               final_status, msg->status_code);
341                 errors++;
342         }
343
344         g_object_unref (req);
345         debug_printf (2, "\n");
346 }
347
348 static void
349 do_redirect_tests (SoupURI *base_uri)
350 {
351         SoupSession *session;
352         int n;
353
354         session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
355                                          SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_REQUESTER,
356                                          SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
357                                          NULL);
358         debug_printf (1, "Async session, SoupMessage\n");
359         for (n = 0; n < n_tests; n++)
360                 do_message_api_test (session, base_uri, n);
361         debug_printf (1, "\nAsync session, SoupRequest\n");
362         for (n = 0; n < n_tests; n++)
363                 do_request_api_test (session, base_uri, n);
364         soup_test_session_abort_unref (session);
365
366         session = soup_test_session_new (SOUP_TYPE_SESSION_SYNC,
367                                          SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_REQUESTER,
368                                          NULL);
369         debug_printf (1, "\nSync session, SoupMessage\n");
370         for (n = 0; n < n_tests; n++)
371                 do_message_api_test (session, base_uri, n);
372         debug_printf (1, "\nSync session, SoupRequest\n");
373         for (n = 0; n < n_tests; n++)
374                 do_request_api_test (session, base_uri, n);
375         soup_test_session_abort_unref (session);
376 }
377
378 typedef struct {
379         SoupSession *session;
380         SoupMessage *msg1, *msg2;
381         SoupURI *uri1, *uri2;
382         SoupSocket *sock1, *sock2;
383 } ConnectionTestData;
384
385 static void
386 msg2_finished (SoupSession *session, SoupMessage *msg2, gpointer user_data)
387 {
388         if (!SOUP_STATUS_IS_SUCCESSFUL (msg2->status_code)) {
389                 debug_printf (1, "  msg2 failed: %d %s\n",
390                               msg2->status_code, msg2->reason_phrase);
391                 errors++;
392         }
393 }
394
395 static void
396 unpause_msg1 (SoupMessage *msg2, gpointer user_data)
397 {
398        ConnectionTestData *data = user_data;
399
400        if (!data->sock1) {
401                debug_printf (1, "  msg1 has no connection?\n");
402                errors++;
403        } else if (!data->sock2) {
404                debug_printf (1, "  msg2 has no connection?\n");
405                errors++;
406        } else if (data->sock1 == data->sock2) {
407                debug_printf (1, "  Both messages sharing the same connection\n");
408                errors++;
409        }
410
411        soup_session_unpause_message (data->session, data->msg1);
412 }
413
414 static gboolean
415 msg1_just_restarted (gpointer user_data)
416 {
417         ConnectionTestData *data = user_data;
418
419         soup_session_pause_message (data->session, data->msg1);
420
421         data->msg2 = soup_message_new_from_uri ("GET", data->uri2);
422
423         g_signal_connect (data->msg2, "got_body",
424                           G_CALLBACK (unpause_msg1), data);
425
426         soup_session_queue_message (data->session, data->msg2, msg2_finished, data);
427         return FALSE;
428 }
429
430 static void
431 msg1_about_to_restart (SoupMessage *msg1, gpointer user_data)
432 {
433         ConnectionTestData *data = user_data;
434
435         /* Do nothing when loading the redirected-to resource */
436         if (!SOUP_STATUS_IS_REDIRECTION (data->msg1->status_code))
437                 return;
438
439         /* We have to pause msg1 after the I/O finishes, but before
440          * the queue runs again.
441          */
442         g_idle_add_full (G_PRIORITY_HIGH, msg1_just_restarted, data, NULL);
443 }
444
445 static void
446 request_started (SoupSession *session, SoupMessage *msg,
447                  SoupSocket *socket, gpointer user_data)
448 {
449         ConnectionTestData *data = user_data;
450
451         if (msg == data->msg1)
452                 data->sock1 = socket;
453         else if (msg == data->msg2)
454                 data->sock2 = socket;
455         else
456                 g_warn_if_reached ();
457 }
458
459 static void
460 do_connection_test (SoupURI *base_uri)
461 {
462         ConnectionTestData data;
463
464         debug_printf (1, "\nConnection reuse\n");
465         memset (&data, 0, sizeof (data));
466
467         data.session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
468         g_signal_connect (data.session, "request-started",
469                           G_CALLBACK (request_started), &data);
470
471         data.uri1 = soup_uri_new_with_base (base_uri, "/301");
472         data.uri2 = soup_uri_new_with_base (base_uri, "/");
473         data.msg1 = soup_message_new_from_uri ("GET", data.uri1);
474
475         g_signal_connect (data.msg1, "got-body",
476                           G_CALLBACK (msg1_about_to_restart), &data);
477         soup_session_send_message (data.session, data.msg1);
478
479         if (!SOUP_STATUS_IS_SUCCESSFUL (data.msg1->status_code)) {
480                 debug_printf (1, "  msg1 failed: %d %s\n",
481                               data.msg1->status_code, data.msg1->reason_phrase);
482                 errors++;
483         }
484         g_object_unref (data.msg1);
485         soup_uri_free (data.uri1);
486         soup_uri_free (data.uri2);
487
488         soup_test_session_abort_unref (data.session);
489 }
490
491 static void
492 server_callback (SoupServer *server, SoupMessage *msg,
493                  const char *path, GHashTable *query,
494                  SoupClientContext *context, gpointer data)
495 {
496         char *remainder;
497         guint status_code;
498
499         /* Make sure that a HTTP/1.0 redirect doesn't cause an
500          * HTTP/1.0 re-request. (#521848)
501          */
502         if (soup_message_get_http_version (msg) == SOUP_HTTP_1_0) {
503                 soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
504                 return;
505         }
506
507         if (g_str_has_prefix (path, "/bad")) {
508                 if (!strcmp (path, "/bad")) {
509                         soup_message_set_status (msg, SOUP_STATUS_FOUND);
510                         soup_message_headers_replace (msg->response_headers,
511                                                       "Location",
512                                                       "/bad with spaces");
513                 } else if (!strcmp (path, "/bad-recursive")) {
514                         soup_message_set_status (msg, SOUP_STATUS_FOUND);
515                         soup_message_headers_replace (msg->response_headers,
516                                                       "Location",
517                                                       "/bad-recursive");
518                 } else if (!strcmp (path, "/bad-no-host")) {
519                         soup_message_set_status (msg, SOUP_STATUS_FOUND);
520                         soup_message_headers_replace (msg->response_headers,
521                                                       "Location",
522                                                       "about:blank");
523                 } else if (!strcmp (path, "/bad with spaces"))
524                         soup_message_set_status (msg, SOUP_STATUS_OK);
525                 else
526                         soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
527                 return;
528         } else if (!strcmp (path, "/server2")) {
529                 soup_message_set_status (msg, SOUP_STATUS_FOUND);
530                 soup_message_headers_replace (msg->response_headers,
531                                               "Location",
532                                               server2_uri);
533                 return;
534         } else if (!strcmp (path, "/")) {
535                 if (msg->method != SOUP_METHOD_GET &&
536                     msg->method != SOUP_METHOD_HEAD) {
537                         soup_message_set_status (msg, SOUP_STATUS_METHOD_NOT_ALLOWED);
538                         return;
539                 }
540
541                 /* Make sure that redirecting a POST clears the body */
542                 if (msg->request_body->length) {
543                         soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
544                         return;
545                 }
546
547                 soup_message_set_status (msg, SOUP_STATUS_OK);
548
549                 /* FIXME: this is wrong, though it doesn't matter for
550                  * the purposes of this test, and to do the right
551                  * thing currently we'd have to set Content-Length by
552                  * hand.
553                  */
554                 if (msg->method != SOUP_METHOD_HEAD) {
555                         soup_message_set_response (msg, "text/plain",
556                                                    SOUP_MEMORY_STATIC,
557                                                    "OK\r\n", 4);
558                 }
559                 return;
560         }
561
562         status_code = strtoul (path + 1, &remainder, 10);
563         if (!SOUP_STATUS_IS_REDIRECTION (status_code) ||
564             (*remainder && *remainder != '/')) {
565                 soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
566                 return;
567         }
568
569         /* See above comment re bug 521848. We only test this on the
570          * double-redirects so that we get connection-reuse testing
571          * the rest of the time.
572          */
573         if (*remainder == '/')
574                 soup_message_set_http_version (msg, SOUP_HTTP_1_0);
575
576         soup_message_set_redirect (msg, status_code,
577                                    *remainder ? remainder : "/");
578 }
579
580 static void
581 server2_callback (SoupServer *server, SoupMessage *msg,
582                   const char *path, GHashTable *query,
583                   SoupClientContext *context, gpointer data)
584 {
585         soup_message_set_status (msg, SOUP_STATUS_OK);
586 }
587
588 static gboolean run_tests = TRUE;
589
590 static GOptionEntry no_test_entry[] = {
591         { "no-tests", 'n', G_OPTION_FLAG_REVERSE,
592           G_OPTION_ARG_NONE, &run_tests,
593           "Don't run tests, just run the test server", NULL },
594         { NULL }
595 };
596
597 int
598 main (int argc, char **argv)
599 {
600         GMainLoop *loop;
601         SoupServer *server, *server2;
602         guint port;
603         SoupURI *base_uri;
604
605         test_init (argc, argv, no_test_entry);
606
607         server = soup_test_server_new (TRUE);
608         soup_server_add_handler (server, NULL,
609                                  server_callback, NULL, NULL);
610         port = soup_server_get_port (server);
611
612         server2 = soup_test_server_new (TRUE);
613         soup_server_add_handler (server2, NULL,
614                                  server2_callback, NULL, NULL);
615         server2_uri = g_strdup_printf ("http://127.0.0.1:%d/on-server2",
616                                        soup_server_get_port (server2));
617
618         loop = g_main_loop_new (NULL, TRUE);
619
620         if (run_tests) {
621                 base_uri = soup_uri_new ("http://127.0.0.1");
622                 soup_uri_set_port (base_uri, port);
623                 do_redirect_tests (base_uri);
624                 do_connection_test (base_uri);
625                 soup_uri_free (base_uri);
626         } else {
627                 printf ("Listening on port %d\n", port);
628                 g_main_loop_run (loop);
629         }
630
631         g_main_loop_unref (loop);
632         g_free (server2_uri);
633         soup_test_server_quit_unref (server);
634         soup_test_server_quit_unref (server2);
635
636         if (run_tests)
637                 test_cleanup ();
638         return errors != 0;
639 }