1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2011 Collabora Ltd.
4  */
5 
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 
10 #include "test-utils.h"
11 
12 #ifndef G_OS_WIN32
13 #include <unistd.h>
14 #endif
15 
16 #define READ_BUFFER_SIZE 8192
17 
18 typedef enum {
19 	NO_MULTIPART,
20 	SYNC_MULTIPART,
21 	ASYNC_MULTIPART,
22 	ASYNC_MULTIPART_SMALL_READS
23 } MultipartMode;
24 
25 char *buffer;
26 SoupSession *session;
27 char *base_uri_string;
28 GUri *base_uri;
29 SoupMultipartInputStream *multipart;
30 unsigned passes;
31 GMainLoop *loop;
32 
33 
34 /* This payload contains 4 different responses.
35  *
36  * First, a text/html response with a Content-Length (31);
37  * Second, a response lacking Content-Type with Content-Length (11);
38  * Third, a text/css response with no Content-Length;
39  * Fourth, same as the third, but with different content;
40  */
41 const char *payload = \
42 	"--cut-here\r\n" \
43 	"Content-Type: text/html\n"
44 	"Content-Length: 30\r\n" \
45 	"\r\n" \
46 	"<html><body>Hey!</body></html>" \
47 	"\r\n--cut-here\r\n" \
48 	"Content-Length: 10\r\n" \
49 	"\r\n" \
50 	"soup rocks" \
51 	"\r\n--cut-here\r\n" \
52 	"Content-Type: text/css\r\n" \
53 	"\r\n" \
54 	".soup { before: rocks; }" \
55 	"\r\n--cut-here\n" /* Tests boundary ending in a single \n. */ \
56 	"Content-Type: text/css\r\n" \
57 	"\r\n" \
58 	"#soup { background-color: black; }" \
59         "\r\n--cut-here--";
60 
61 static void
server_callback(SoupServer * server,SoupServerMessage * msg,const char * path,GHashTable * query,gpointer data)62 server_callback (SoupServer        *server,
63 		 SoupServerMessage *msg,
64 		 const char        *path,
65 		 GHashTable        *query,
66 		 gpointer           data)
67 {
68 	SoupMessageHeaders *response_headers;
69 	SoupMessageBody *response_body;
70 
71 	if (soup_server_message_get_method (msg) != SOUP_METHOD_GET) {
72 		soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, NULL);
73 		return;
74 	}
75 
76 	soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
77 
78 	response_headers = soup_server_message_get_response_headers (msg);
79 	soup_message_headers_append (response_headers,
80 				     "Content-Type", "multipart/x-mixed-replace; boundary=cut-here");
81 
82 	response_body = soup_server_message_get_response_body (msg);
83 	soup_message_body_append (response_body,
84 				  SOUP_MEMORY_STATIC,
85 				  payload,
86 				  strlen (payload));
87 
88 	soup_message_body_complete (response_body);
89 }
90 
91 static void
content_sniffed(SoupMessage * msg,char * content_type,GHashTable * params,int * sniffed_count)92 content_sniffed (SoupMessage *msg, char *content_type, GHashTable *params, int *sniffed_count)
93 {
94 	*sniffed_count = *sniffed_count + 1;
95 	debug_printf (2, "  content-sniffed -> %s\n", content_type);
96 }
97 
98 static void
check_is_next(gboolean is_next)99 check_is_next (gboolean is_next)
100 {
101 	soup_test_assert (is_next,
102 			  "expected a header, but there are no more headers");
103 }
104 
105 static void
got_headers(SoupMessage * msg,int * headers_count)106 got_headers (SoupMessage *msg, int *headers_count)
107 {
108 	SoupMessageHeadersIter iter;
109 	gboolean is_next;
110 	const char* name, *value;
111 
112 	*headers_count = *headers_count + 1;
113 
114 	soup_message_headers_iter_init (&iter, soup_message_get_response_headers (msg));
115 
116 	is_next = soup_message_headers_iter_next (&iter, &name, &value);
117 	check_is_next (is_next);
118 
119 	if (g_str_equal (name, "Date")) {
120 		is_next = soup_message_headers_iter_next (&iter, &name, &value);
121 		check_is_next (is_next);
122 	}
123 
124 	g_assert_cmpstr (name, ==, "Content-Type");
125 	g_assert_cmpstr (value, ==, "multipart/x-mixed-replace; boundary=cut-here");
126 }
127 
128 static void
read_cb(GObject * source,GAsyncResult * asyncResult,gpointer data)129 read_cb (GObject *source, GAsyncResult *asyncResult, gpointer data)
130 {
131 	GInputStream *stream = G_INPUT_STREAM (source);
132 	GError *error = NULL;
133 	gssize bytes_read;
134 
135 	bytes_read = g_input_stream_read_finish (stream, asyncResult, &error);
136 	g_assert_no_error (error);
137 	if (error) {
138 		g_object_unref (stream);
139 		g_main_loop_quit (loop);
140 		return;
141 	}
142 
143 	if (!bytes_read) {
144 		g_input_stream_close (stream, NULL, &error);
145 		g_assert_no_error (error);
146 		g_object_unref (stream);
147 		g_main_loop_quit (loop);
148 		return;
149 	}
150 
151 	g_input_stream_read_async (stream, buffer, READ_BUFFER_SIZE,
152 				   G_PRIORITY_DEFAULT, NULL,
153 				   read_cb, NULL);
154 }
155 
156 static void
no_multipart_handling_cb(GObject * source,GAsyncResult * res,gpointer data)157 no_multipart_handling_cb (GObject *source, GAsyncResult *res, gpointer data)
158 {
159 	SoupSession *session = SOUP_SESSION (source);
160 	GError *error = NULL;
161 	GInputStream* in;
162 
163 	in = soup_session_send_finish (session, res, &error);
164 	g_assert_no_error (error);
165 	if (error) {
166 		g_main_loop_quit (loop);
167 		return;
168 	}
169 
170 	g_input_stream_read_async (in, buffer, READ_BUFFER_SIZE,
171 				   G_PRIORITY_DEFAULT, NULL,
172 				   read_cb, NULL);
173 }
174 
175 static void
multipart_close_part_cb(GObject * source,GAsyncResult * res,gpointer data)176 multipart_close_part_cb (GObject *source, GAsyncResult *res, gpointer data)
177 {
178 	GInputStream *in = G_INPUT_STREAM (source);
179 	GError *error = NULL;
180 
181 	g_input_stream_close_finish (in, res, &error);
182 	g_assert_no_error (error);
183 }
184 
185 static void multipart_next_part_cb (GObject *source,
186 				    GAsyncResult *res,
187 				    gpointer data);
188 
189 static void
check_read(gsize nread,unsigned passes)190 check_read (gsize nread, unsigned passes)
191 {
192 	switch (passes) {
193 	case 0:
194 		g_assert_cmpint (nread, ==, 30);
195 		break;
196 	case 1:
197 		g_assert_cmpint (nread, ==, 10);
198 		break;
199 	case 2:
200 		g_assert_cmpint (nread, ==, 24);
201 		break;
202 	case 3:
203 		g_assert_cmpint (nread, ==, 34);
204 		break;
205 	default:
206 		soup_test_assert (FALSE, "unexpected read of size: %d", (int)nread);
207 		break;
208 	}
209 }
210 
211 static void
multipart_read_cb(GObject * source,GAsyncResult * asyncResult,gpointer data)212 multipart_read_cb (GObject *source, GAsyncResult *asyncResult, gpointer data)
213 {
214 	GInputStream *in = G_INPUT_STREAM (source);
215 	GError *error = NULL;
216 	static gssize bytes_read_for_part = 0;
217 	gssize bytes_read;
218 
219 	bytes_read = g_input_stream_read_finish (in, asyncResult, &error);
220 	g_assert_no_error (error);
221 	if (error) {
222 		g_input_stream_close_async (in, G_PRIORITY_DEFAULT, NULL,
223 					    multipart_close_part_cb, NULL);
224 		g_object_unref (in);
225 
226 		g_main_loop_quit (loop);
227 		return;
228 	}
229 
230 	/* Read 0 bytes - try to start reading another part. */
231 	if (!bytes_read) {
232 		check_read (bytes_read_for_part, passes);
233 		bytes_read_for_part = 0;
234 		passes++;
235 
236 		g_input_stream_close_async (in, G_PRIORITY_DEFAULT, NULL,
237 					    multipart_close_part_cb, NULL);
238 		g_object_unref (in);
239 
240 		soup_multipart_input_stream_next_part_async (multipart, G_PRIORITY_DEFAULT, NULL,
241 							     multipart_next_part_cb, NULL);
242 		return;
243 	}
244 
245 	bytes_read_for_part += bytes_read;
246 	g_input_stream_read_async (in, buffer, READ_BUFFER_SIZE,
247 				   G_PRIORITY_DEFAULT, NULL,
248 				   multipart_read_cb, NULL);
249 }
250 
251 static void
check_headers(SoupMultipartInputStream * multipart,unsigned passes)252 check_headers (SoupMultipartInputStream* multipart, unsigned passes)
253 {
254 	SoupMessageHeaders *headers;
255 	SoupMessageHeadersIter iter;
256 	gboolean is_next;
257 	const char *name, *value;
258 
259 	headers = soup_multipart_input_stream_get_headers (multipart);
260 	soup_message_headers_iter_init (&iter, headers);
261 
262 	switch (passes) {
263 	case 0:
264 		is_next = soup_message_headers_iter_next (&iter, &name, &value);
265 		check_is_next (is_next);
266 
267 		g_assert_cmpstr (name, ==, "Content-Type");
268 		g_assert_cmpstr (value, ==, "text/html");
269 
270 		is_next = soup_message_headers_iter_next (&iter, &name, &value);
271 		check_is_next (is_next);
272 
273 		g_assert_cmpstr (name, ==, "Content-Length");
274 		g_assert_cmpstr (value, ==, "30");
275 
276 		break;
277 	case 1:
278 		is_next = soup_message_headers_iter_next (&iter, &name, &value);
279 		check_is_next (is_next);
280 
281 		g_assert_cmpstr (name, ==, "Content-Length");
282 		g_assert_cmpstr (value, ==, "10");
283 
284 		break;
285 	case 2:
286 	case 3:
287 		is_next = soup_message_headers_iter_next (&iter, &name, &value);
288 		check_is_next (is_next);
289 
290 		g_assert_cmpstr (name, ==, "Content-Type");
291 		g_assert_cmpstr (value, ==, "text/css");
292 
293 		break;
294 	default:
295 		soup_test_assert (FALSE, "unexpected part received");
296 		break;
297 	}
298 }
299 
300 static void
multipart_next_part_cb(GObject * source,GAsyncResult * res,gpointer data)301 multipart_next_part_cb (GObject *source, GAsyncResult *res, gpointer data)
302 {
303 	GError *error = NULL;
304 	GInputStream *in;
305 	gsize read_size = READ_BUFFER_SIZE;
306 
307 	g_assert (SOUP_MULTIPART_INPUT_STREAM (source) == multipart);
308 
309 	in = soup_multipart_input_stream_next_part_finish (multipart, res, &error);
310 	g_assert_no_error (error);
311 	if (error) {
312 		g_clear_error (&error);
313 		g_object_unref (multipart);
314 		g_main_loop_quit (loop);
315 		return;
316 	}
317 
318 	if (!in) {
319 		g_assert_cmpint (passes, ==, 4);
320 		g_object_unref (multipart);
321 		g_main_loop_quit (loop);
322 		return;
323 	}
324 
325 	check_headers (multipart, passes);
326 
327 	if (g_object_get_data (G_OBJECT (multipart), "multipart-small-reads"))
328 		read_size = 4;
329 
330 	g_input_stream_read_async (in, buffer, read_size,
331 				   G_PRIORITY_DEFAULT, NULL,
332 				   multipart_read_cb, data);
333 }
334 
335 static void
multipart_handling_cb(GObject * source,GAsyncResult * res,gpointer data)336 multipart_handling_cb (GObject *source, GAsyncResult *res, gpointer data)
337 {
338 	SoupMessage *message;
339 	SoupSession *session = SOUP_SESSION (source);
340 	GError *error = NULL;
341 	GInputStream *in;
342 
343 	in = soup_session_send_finish (session, res, &error);
344 	g_assert_no_error (error);
345 	if (error) {
346 		g_main_loop_quit (loop);
347 		return;
348 	}
349 
350 	message = soup_session_get_async_result_message (session, res);
351 	multipart = soup_multipart_input_stream_new (message, in);
352 	g_object_unref (in);
353 
354 	if (g_object_get_data (G_OBJECT (message), "multipart-small-reads"))
355 		g_object_set_data (G_OBJECT (multipart), "multipart-small-reads", GINT_TO_POINTER(1));
356 
357 	soup_multipart_input_stream_next_part_async (multipart, G_PRIORITY_DEFAULT, NULL,
358 						     multipart_next_part_cb, NULL);
359 }
360 
361 static void
sync_multipart_handling_cb(GObject * source,GAsyncResult * res,gpointer data)362 sync_multipart_handling_cb (GObject *source, GAsyncResult *res, gpointer data)
363 {
364 	SoupMessage *message;
365 	SoupSession *session = SOUP_SESSION (source);
366 	GError *error = NULL;
367 	GInputStream *in;
368 	char buffer[READ_BUFFER_SIZE];
369 	gsize bytes_read;
370 
371 	in = soup_session_send_finish (session, res, &error);
372 	g_assert_no_error (error);
373 	if (error) {
374 		g_main_loop_quit (loop);
375 		return;
376 	}
377 
378 	message = soup_session_get_async_result_message (session, res);
379 	multipart = soup_multipart_input_stream_new (message, in);
380 	g_object_unref (in);
381 
382 	while (TRUE) {
383 		in = soup_multipart_input_stream_next_part (multipart, NULL, &error);
384 		g_assert_no_error (error);
385 		if (error) {
386 			g_clear_error (&error);
387 			break;
388 		}
389 
390 		if (!in)
391 			break;
392 
393 		check_headers (multipart, passes);
394 
395 		g_input_stream_read_all (in, (void*)buffer, sizeof (buffer), &bytes_read, NULL, &error);
396 		g_assert_no_error (error);
397 		if (error) {
398 			g_clear_error (&error);
399 			g_object_unref (in);
400 			break;
401 		}
402 
403 		check_read (bytes_read, passes);
404 
405 		passes++;
406 		g_object_unref (in);
407 	}
408 
409 	g_assert_cmpint (passes, ==, 4);
410 
411 	g_main_loop_quit (loop);
412 	g_object_unref (multipart);
413 }
414 
415 static void
test_multipart(gconstpointer data)416 test_multipart (gconstpointer data)
417 {
418 	int headers_expected = 1, sniffed_expected = 1;
419 	MultipartMode multipart_mode = GPOINTER_TO_INT (data);
420 	SoupMessage *msg;
421 	int headers_count = 0;
422 	int sniffed_count = 0;
423 	GHashTable *params;
424 	const char *content_type;
425 	gboolean message_is_multipart = FALSE;
426 
427 	msg = soup_message_new ("GET", base_uri_string);
428 
429 	/* This is used to track the number of parts. */
430 	passes = 0;
431 
432 	/* Force the server to close the connection. */
433 	soup_message_headers_append (soup_message_get_request_headers (msg),
434 				     "Connection", "close");
435 
436 	g_signal_connect (msg, "got_headers",
437 			  G_CALLBACK (got_headers), &headers_count);
438 
439 	g_signal_connect (msg, "content-sniffed",
440 			  G_CALLBACK (content_sniffed), &sniffed_count);
441 
442 	loop = g_main_loop_new (NULL, TRUE);
443 
444 	if (multipart_mode == ASYNC_MULTIPART)
445 		soup_session_send_async (session, msg, 0, NULL, multipart_handling_cb, NULL);
446 	else if (multipart_mode == ASYNC_MULTIPART_SMALL_READS) {
447 		g_object_set_data (G_OBJECT (msg), "multipart-small-reads", GINT_TO_POINTER(1));
448 		soup_session_send_async (session, msg, 0, NULL, multipart_handling_cb, NULL);
449 	} else if (multipart_mode == SYNC_MULTIPART)
450 		soup_session_send_async (session, msg, 0, NULL, sync_multipart_handling_cb, NULL);
451 	else
452 		soup_session_send_async (session, msg, 0, NULL, no_multipart_handling_cb, NULL);
453 
454 	g_main_loop_run (loop);
455 	while (g_main_context_pending (NULL))
456 		g_main_context_iteration (NULL, FALSE);
457 
458 	content_type = soup_message_headers_get_content_type (soup_message_get_response_headers (msg), &params);
459 
460 	if (content_type &&
461 	    g_str_has_prefix (content_type, "multipart/") &&
462 	    g_hash_table_lookup (params, "boundary")) {
463 		message_is_multipart = TRUE;
464 	}
465 	g_clear_pointer (&params, g_hash_table_unref);
466 
467 	g_assert_true (message_is_multipart);
468 	g_assert_cmpint (headers_count, ==, headers_expected);
469 	g_assert_cmpint (sniffed_count, ==, sniffed_expected);
470 
471 	g_object_unref (msg);
472 	g_main_loop_unref (loop);
473 	loop = NULL;
474 }
475 
476 int
main(int argc,char ** argv)477 main (int argc, char **argv)
478 {
479 	SoupServer *server;
480 	int ret;
481 
482 	test_init (argc, argv, NULL);
483 
484 	buffer = g_malloc (READ_BUFFER_SIZE);
485 
486 	server = soup_test_server_new (SOUP_TEST_SERVER_DEFAULT);
487 	soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
488 	base_uri = soup_test_server_get_uri (server, "http", NULL);
489 	base_uri_string = g_uri_to_string (base_uri);
490 
491 	/* FIXME: I had to raise the number of connections allowed here, otherwise I
492 	 * was hitting the limit, which indicates some connections are not dying.
493 	 */
494 	session = soup_test_session_new ("max-conns", 20,
495 					 "max-conns-per-host", 20,
496 					 NULL);
497 	soup_session_add_feature_by_type (session, SOUP_TYPE_CONTENT_SNIFFER);
498 
499 	g_test_add_data_func ("/multipart/no", GINT_TO_POINTER (NO_MULTIPART), test_multipart);
500 	g_test_add_data_func ("/multipart/sync", GINT_TO_POINTER (SYNC_MULTIPART), test_multipart);
501 	g_test_add_data_func ("/multipart/async", GINT_TO_POINTER (ASYNC_MULTIPART), test_multipart);
502 	g_test_add_data_func ("/multipart/async-small-reads", GINT_TO_POINTER (ASYNC_MULTIPART_SMALL_READS), test_multipart);
503 
504 	ret = g_test_run ();
505 
506 	g_uri_unref (base_uri);
507 	g_free (base_uri_string);
508 	g_free (buffer);
509 
510 	soup_test_session_abort_unref (session);
511 	soup_test_server_quit_unref (server);
512 
513 	test_cleanup ();
514 	return ret;
515 }
516