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