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