1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 
3 #include "test-utils.h"
4 
5 static SoupBuffer *correct_response;
6 
7 static void
authenticate(SoupSession * session,SoupMessage * msg,SoupAuth * auth,gboolean retrying,gpointer data)8 authenticate (SoupSession *session, SoupMessage *msg,
9 	      SoupAuth *auth, gboolean retrying, gpointer data)
10 {
11 	if (!retrying)
12 		soup_auth_authenticate (auth, "user2", "realm2");
13 }
14 
15 #ifdef HAVE_APACHE
16 static void
get_correct_response(const char * uri)17 get_correct_response (const char *uri)
18 {
19 	SoupSession *session;
20 	SoupMessage *msg;
21 
22 	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
23 	msg = soup_message_new (SOUP_METHOD_GET, uri);
24 	soup_session_send_message (session, msg);
25 	if (msg->status_code != SOUP_STATUS_OK) {
26 		g_printerr ("Could not fetch %s: %d %s\n", uri,
27 			    msg->status_code, msg->reason_phrase);
28 		exit (1);
29 	}
30 
31 	correct_response = soup_message_body_flatten (msg->response_body);
32 
33 	g_object_unref (msg);
34 	soup_test_session_abort_unref (session);
35 }
36 #endif
37 
38 /* Pull API version 1: fully-async. More like a "poke" API. Rather
39  * than having SoupMessage emit "got_chunk" signals whenever it wants,
40  * we stop it after it finishes reading the message headers, and then
41  * tell it when we want to hear about new chunks.
42  */
43 
44 typedef struct {
45 	GMainLoop *loop;
46 	SoupSession *session;
47 	SoupMessage *msg;
48 	guint timeout;
49 	gboolean chunks_ready;
50 	gboolean chunk_wanted;
51 	gboolean did_first_timeout;
52 	gsize read_so_far;
53 	guint expected_status;
54 } FullyAsyncData;
55 
56 static void fully_async_got_headers (SoupMessage *msg, gpointer user_data);
57 static void fully_async_got_chunk   (SoupMessage *msg, SoupBuffer *chunk,
58 				     gpointer user_data);
59 static void fully_async_finished    (SoupSession *session, SoupMessage *msg,
60 				     gpointer user_data);
61 static gboolean fully_async_request_chunk (gpointer user_data);
62 
63 static void
do_fully_async_test(SoupSession * session,const char * base_uri,const char * sub_uri,gboolean fast_request,guint expected_status)64 do_fully_async_test (SoupSession *session,
65 		     const char *base_uri, const char *sub_uri,
66 		     gboolean fast_request, guint expected_status)
67 {
68 	GMainLoop *loop;
69 	FullyAsyncData ad;
70 	SoupMessage *msg;
71 	char *uri;
72 
73 	loop = g_main_loop_new (NULL, FALSE);
74 
75 	uri = g_build_filename (base_uri, sub_uri, NULL);
76 	debug_printf (1, "GET %s\n", uri);
77 
78 	msg = soup_message_new (SOUP_METHOD_GET, uri);
79 	g_free (uri);
80 
81 	ad.loop = loop;
82 	ad.session = session;
83 	ad.msg = msg;
84 	ad.chunks_ready = FALSE;
85 	ad.chunk_wanted = FALSE;
86 	ad.did_first_timeout = FALSE;
87 	ad.read_so_far = 0;
88 	ad.expected_status = expected_status;
89 
90 	/* Since we aren't going to look at the final value of
91 	 * msg->response_body, we tell libsoup to not even bother
92 	 * generating it.
93 	 */
94 	soup_message_body_set_accumulate (msg->response_body, FALSE);
95 
96 	/* Connect to "got_headers", from which we'll decide where to
97 	 * go next.
98 	 */
99 	g_signal_connect (msg, "got_headers",
100 			  G_CALLBACK (fully_async_got_headers), &ad);
101 
102 	/* Queue the request */
103 	soup_session_queue_message (session, msg, fully_async_finished, &ad);
104 
105 	/* In a real program, we'd probably just return at this point.
106 	 * Eventually the caller would return all the way to the main
107 	 * loop, and then eventually, some event would cause the
108 	 * application to request a chunk of data from the message
109 	 * response.
110 	 *
111 	 * In our test program, there is no "real" main loop, so we
112 	 * had to create our own. We use a timeout to represent the
113 	 * event that causes the app to decide to request another body
114 	 * chunk. We use short timeouts in one set of tests, and long
115 	 * ones in another, to test both the
116 	 * chunk-requested-before-its-been-read and
117 	 * chunk-read-before-its-been-requested cases.
118 	 */
119 	ad.timeout = g_timeout_add (fast_request ? 0 : 100,
120 				    fully_async_request_chunk, &ad);
121 	g_main_loop_run (ad.loop);
122 	g_main_loop_unref (ad.loop);
123 }
124 
125 static gboolean
fully_async_request_chunk(gpointer user_data)126 fully_async_request_chunk (gpointer user_data)
127 {
128 	FullyAsyncData *ad = user_data;
129 
130 	if (!ad->did_first_timeout) {
131 		debug_printf (1, "  first timeout\n");
132 		ad->did_first_timeout = TRUE;
133 	} else
134 		debug_printf (2, "  timeout\n");
135 	ad->timeout = 0;
136 
137 	/* ad->chunks_ready and ad->chunk_wanted are used because
138 	 * there's a race condition between the application requesting
139 	 * the first chunk, and the message reaching a point where
140 	 * it's actually ready to read chunks. If chunks_ready has
141 	 * been set, we can just call soup_session_unpause_message() to
142 	 * cause the first chunk to be read. But if it's not, we just
143 	 * set chunk_wanted, to let the got_headers handler below know
144 	 * that a chunk has already been requested.
145 	 */
146 	if (ad->chunks_ready)
147 		soup_session_unpause_message (ad->session, ad->msg);
148 	else
149 		ad->chunk_wanted = TRUE;
150 
151 	return FALSE;
152 }
153 
154 static void
fully_async_got_headers(SoupMessage * msg,gpointer user_data)155 fully_async_got_headers (SoupMessage *msg, gpointer user_data)
156 {
157 	FullyAsyncData *ad = user_data;
158 
159 	debug_printf (1, "  %d %s\n", msg->status_code, msg->reason_phrase);
160 	if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) {
161 		/* Let soup handle this one; this got_headers handler
162 		 * will get called again next time around.
163 		 */
164 		return;
165 	} else if (msg->status_code != SOUP_STATUS_OK) {
166 		soup_test_assert_message_status (msg, SOUP_STATUS_OK);
167 		return;
168 	}
169 
170 	/* OK, we're happy with the response. So, we connect to
171 	 * "got_chunk". If there has already been a chunk requested,
172 	 * we let I/O continue; but if there hasn't, we pause now
173 	 * until one is requested.
174 	 */
175 	ad->chunks_ready = TRUE;
176 	g_signal_connect (msg, "got_chunk",
177 			  G_CALLBACK (fully_async_got_chunk), ad);
178 	if (!ad->chunk_wanted)
179 		soup_session_pause_message (ad->session, msg);
180 }
181 
182 static void
fully_async_got_chunk(SoupMessage * msg,SoupBuffer * chunk,gpointer user_data)183 fully_async_got_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
184 {
185 	FullyAsyncData *ad = user_data;
186 
187 	debug_printf (2, "  got chunk from %lu - %lu\n",
188 		      (unsigned long) ad->read_so_far,
189 		      (unsigned long) ad->read_so_far + chunk->length);
190 
191 	/* We've got a chunk, let's process it. In the case of the
192 	 * test program, that means comparing it against
193 	 * correct_response to make sure that we got the right data.
194 	 */
195 	g_assert_cmpint (ad->read_so_far + chunk->length, <=, correct_response->length);
196 	soup_assert_cmpmem (chunk->data, chunk->length,
197 			    correct_response->data + ad->read_so_far,
198 			    chunk->length);
199 	ad->read_so_far += chunk->length;
200 
201 	/* Now pause I/O, and prepare to read another chunk later.
202 	 * (Again, the timeout just abstractly represents the idea of
203 	 * the application requesting another chunk at some random
204 	 * point in the future. You wouldn't be using a timeout in a
205 	 * real program.)
206 	 */
207 	soup_session_pause_message (ad->session, msg);
208 	ad->chunk_wanted = FALSE;
209 
210 	ad->timeout = g_timeout_add (10, fully_async_request_chunk, ad);
211 }
212 
213 static void
fully_async_finished(SoupSession * session,SoupMessage * msg,gpointer user_data)214 fully_async_finished (SoupSession *session, SoupMessage *msg,
215 		      gpointer user_data)
216 {
217 	FullyAsyncData *ad = user_data;
218 
219 	soup_test_assert_message_status (msg, ad->expected_status);
220 
221 	if (ad->timeout != 0)
222 		g_source_remove (ad->timeout);
223 
224 	/* Since our test program is only running the loop for the
225 	 * purpose of this one test, we quit the loop once the
226 	 * test is done.
227 	 */
228 	g_main_loop_quit (ad->loop);
229 }
230 
231 static void
do_fast_async_test(gconstpointer data)232 do_fast_async_test (gconstpointer data)
233 {
234 	const char *base_uri = data;
235 	SoupSession *session;
236 
237 	SOUP_TEST_SKIP_IF_NO_APACHE;
238 
239 	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
240 	g_signal_connect (session, "authenticate",
241 			  G_CALLBACK (authenticate), NULL);
242 	do_fully_async_test (session, base_uri, "/",
243 			     TRUE, SOUP_STATUS_OK);
244 	do_fully_async_test (session, base_uri, "/Basic/realm1/",
245 			     TRUE, SOUP_STATUS_UNAUTHORIZED);
246 	do_fully_async_test (session, base_uri, "/Basic/realm2/",
247 			     TRUE, SOUP_STATUS_OK);
248 	soup_test_session_abort_unref (session);
249 }
250 
251 static void
do_slow_async_test(gconstpointer data)252 do_slow_async_test (gconstpointer data)
253 {
254 	const char *base_uri = data;
255 	SoupSession *session;
256 
257 	SOUP_TEST_SKIP_IF_NO_APACHE;
258 
259 	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
260 	g_signal_connect (session, "authenticate",
261 			  G_CALLBACK (authenticate), NULL);
262 	do_fully_async_test (session, base_uri, "/",
263 			     FALSE, SOUP_STATUS_OK);
264 	do_fully_async_test (session, base_uri, "/Basic/realm1/",
265 			     FALSE, SOUP_STATUS_UNAUTHORIZED);
266 	do_fully_async_test (session, base_uri, "/Basic/realm2/",
267 			     FALSE, SOUP_STATUS_OK);
268 	soup_test_session_abort_unref (session);
269 }
270 
271 /* Pull API version 2: synchronous pull API via async I/O. */
272 
273 typedef struct {
274 	GMainLoop *loop;
275 	SoupSession *session;
276 	SoupBuffer *chunk;
277 } SyncAsyncData;
278 
279 static void        sync_async_send       (SoupSession *session,
280 					  SoupMessage *msg);
281 static gboolean    sync_async_is_finished(SoupMessage *msg);
282 static SoupBuffer *sync_async_read_chunk (SoupMessage *msg);
283 static void        sync_async_cleanup    (SoupMessage *msg);
284 
285 static void sync_async_got_headers (SoupMessage *msg, gpointer user_data);
286 static void sync_async_copy_chunk  (SoupMessage *msg, SoupBuffer *chunk,
287 				    gpointer user_data);
288 static void sync_async_finished    (SoupSession *session, SoupMessage *msg,
289 				    gpointer user_data);
290 
291 static void
do_synchronously_async_test(SoupSession * session,const char * base_uri,const char * sub_uri,guint expected_status)292 do_synchronously_async_test (SoupSession *session,
293 			     const char *base_uri, const char *sub_uri,
294 			     guint expected_status)
295 {
296 	SoupMessage *msg;
297 	char *uri;
298 	gsize read_so_far;
299 	SoupBuffer *chunk;
300 
301 	uri = g_build_filename (base_uri, sub_uri, NULL);
302 	debug_printf (1, "GET %s\n", uri);
303 
304 	msg = soup_message_new (SOUP_METHOD_GET, uri);
305 	g_free (uri);
306 
307 	/* As in the fully-async case, we turn off accumulate, as an
308 	 * optimization.
309 	 */
310 	soup_message_body_set_accumulate (msg->response_body, FALSE);
311 
312 	/* Send the message, get back headers */
313 	sync_async_send (session, msg);
314 	if (expected_status == SOUP_STATUS_OK) {
315 		soup_test_assert (!sync_async_is_finished (msg),
316 				  "finished without reading response");
317 	} else {
318 		soup_test_assert (sync_async_is_finished (msg),
319 				  "request failed to fail");
320 	}
321 
322 	/* Now we're ready to read the response body (though we could
323 	 * put that off until later if we really wanted).
324 	 */
325 	read_so_far = 0;
326 	while ((chunk = sync_async_read_chunk (msg))) {
327 		debug_printf (2, "  read chunk from %lu - %lu\n",
328 			      (unsigned long) read_so_far,
329 			      (unsigned long) read_so_far + chunk->length);
330 
331 		g_assert_cmpint (read_so_far + chunk->length, <=, correct_response->length);
332 		soup_assert_cmpmem (chunk->data, chunk->length,
333 				    correct_response->data + read_so_far,
334 				    chunk->length);
335 
336 		read_so_far += chunk->length;
337 		soup_buffer_free (chunk);
338 	}
339 
340 	g_assert_true (sync_async_is_finished (msg));
341 	soup_test_assert_message_status (msg, expected_status);
342 	if (msg->status_code == SOUP_STATUS_OK)
343 		g_assert_cmpint (read_so_far, ==, correct_response->length);
344 
345 	sync_async_cleanup (msg);
346 	g_object_unref (msg);
347 }
348 
349 /* Sends @msg on async session @session and returns after the headers
350  * of a successful response (or the complete body of a failed
351  * response) have been read.
352  */
353 static void
sync_async_send(SoupSession * session,SoupMessage * msg)354 sync_async_send (SoupSession *session, SoupMessage *msg)
355 {
356 	SyncAsyncData *ad;
357 
358 	ad = g_new0 (SyncAsyncData, 1);
359 	g_object_set_data (G_OBJECT (msg), "SyncAsyncData", ad);
360 
361 	/* In this case, unlike the fully-async case, the loop
362 	 * actually belongs to us, not the application; it will only
363 	 * be run when we're waiting for chunks, not at other times.
364 	 *
365 	 * If session has an async_context associated with it, we'd
366 	 * want to pass that, rather than NULL, here.
367 	 */
368 	ad->loop = g_main_loop_new (NULL, FALSE);
369 	ad->session = session;
370 
371 	g_signal_connect (msg, "got_headers",
372 			  G_CALLBACK (sync_async_got_headers), ad);
373 
374 	/* Start the request by queuing it and then running our main
375 	 * loop. Note: we have to use soup_session_queue_message()
376 	 * here; soup_session_send_message() won't work, for several
377 	 * reasons. Also, since soup_session_queue_message() steals a
378 	 * ref to the message and then unrefs it after invoking the
379 	 * callback, we have to add an extra ref before calling it.
380 	 */
381 	g_object_ref (msg);
382 	soup_session_queue_message (session, msg, sync_async_finished, ad);
383 	g_main_loop_run (ad->loop);
384 
385 	/* At this point, one of two things has happened; either the
386 	 * got_headers handler got headers it liked, and so stopped
387 	 * the loop, or else the message was fully processed without
388 	 * the got_headers handler interrupting it, and so the final
389 	 * callback (sync_async_finished) was invoked, and stopped the
390 	 * loop.
391 	 *
392 	 * Either way, we're done, so we return to the caller.
393 	 */
394 }
395 
396 static void
sync_async_got_headers(SoupMessage * msg,gpointer user_data)397 sync_async_got_headers (SoupMessage *msg, gpointer user_data)
398 {
399 	SyncAsyncData *ad = user_data;
400 
401 	debug_printf (1, "  %d %s\n", msg->status_code, msg->reason_phrase);
402 	if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) {
403 		/* Let soup handle this one; this got_headers handler
404 		 * will get called again next time around.
405 		 */
406 		return;
407 	} else if (msg->status_code != SOUP_STATUS_OK) {
408 		soup_test_assert_message_status (msg, SOUP_STATUS_OK);
409 		return;
410 	}
411 
412 	/* Stop I/O and return to the caller */
413 	soup_session_pause_message (ad->session, msg);
414 	g_main_loop_quit (ad->loop);
415 }
416 
417 static gboolean
sync_async_is_finished(SoupMessage * msg)418 sync_async_is_finished (SoupMessage *msg)
419 {
420 	SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
421 
422 	/* sync_async_finished clears ad->loop */
423 	return ad->loop == NULL;
424 }
425 
426 /* Tries to read a chunk. Returns %NULL on error/end-of-response. */
427 static SoupBuffer *
sync_async_read_chunk(SoupMessage * msg)428 sync_async_read_chunk (SoupMessage *msg)
429 {
430 	SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
431 	guint handler;
432 
433 	if (sync_async_is_finished (msg))
434 		return NULL;
435 
436 	ad->chunk = NULL;
437 	handler = g_signal_connect (msg, "got_chunk",
438 				    G_CALLBACK (sync_async_copy_chunk),
439 				    ad);
440 	soup_session_unpause_message (ad->session, msg);
441 	g_main_loop_run (ad->loop);
442 	g_signal_handler_disconnect (msg, handler);
443 
444 	return ad->chunk;
445 }
446 
447 static void
sync_async_copy_chunk(SoupMessage * msg,SoupBuffer * chunk,gpointer user_data)448 sync_async_copy_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
449 {
450 	SyncAsyncData *ad = user_data;
451 
452 	ad->chunk = soup_buffer_copy (chunk);
453 
454 	/* Now pause and return from the g_main_loop_run() call in
455 	 * sync_async_read_chunk().
456 	 */
457 	soup_session_pause_message (ad->session, msg);
458 	g_main_loop_quit (ad->loop);
459 }
460 
461 static void
sync_async_finished(SoupSession * session,SoupMessage * msg,gpointer user_data)462 sync_async_finished (SoupSession *session, SoupMessage *msg, gpointer user_data)
463 {
464 	SyncAsyncData *ad = user_data;
465 
466 	/* Unlike in the fully_async_case, we don't need to do much
467 	 * here, because control will return to
468 	 * do_synchronously_async_test() when we're done, and we do
469 	 * the final tests there.
470 	 */
471 	g_main_loop_quit (ad->loop);
472 	g_main_loop_unref (ad->loop);
473 	ad->loop = NULL;
474 }
475 
476 static void
sync_async_cleanup(SoupMessage * msg)477 sync_async_cleanup (SoupMessage *msg)
478 {
479 	SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
480 
481 	if (ad->loop)
482 		g_main_loop_unref (ad->loop);
483 	g_free (ad);
484 }
485 
486 static void
do_sync_async_test(gconstpointer data)487 do_sync_async_test (gconstpointer data)
488 {
489 	const char *base_uri = data;
490 	SoupSession *session;
491 
492 	SOUP_TEST_SKIP_IF_NO_APACHE;
493 
494 	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
495 	g_signal_connect (session, "authenticate",
496 			  G_CALLBACK (authenticate), NULL);
497 	do_synchronously_async_test (session, base_uri, "/",
498 				     SOUP_STATUS_OK);
499 	do_synchronously_async_test (session, base_uri, "/Basic/realm1/",
500 				     SOUP_STATUS_UNAUTHORIZED);
501 	do_synchronously_async_test (session, base_uri, "/Basic/realm2/",
502 				     SOUP_STATUS_OK);
503 	soup_test_session_abort_unref (session);
504 }
505 
506 
507 int
main(int argc,char ** argv)508 main (int argc, char **argv)
509 {
510 	const char *base_uri;
511 	int ret;
512 
513 	test_init (argc, argv, NULL);
514 	apache_init ();
515 
516 	base_uri = "http://127.0.0.1:47524/";
517 #ifdef HAVE_APACHE
518 	get_correct_response (base_uri);
519 #endif
520 
521 	g_test_add_data_func ("/pull-api/async/fast", base_uri, do_fast_async_test);
522 	g_test_add_data_func ("/pull-api/async/slow", base_uri, do_slow_async_test);
523 	g_test_add_data_func ("/pull-api/sync-async", base_uri, do_sync_async_test);
524 
525 	ret = g_test_run ();
526 
527 #ifdef HAVE_APACHE
528 	soup_buffer_free (correct_response);
529 #endif
530 
531 	test_cleanup ();
532 	return ret;
533 }
534