/* * This file is part of Cockpit. * * Copyright (C) 2014 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ #include "config.h" #include "cockpittransport.h" #include "cockpitpipe.h" #include "cockpitpipetransport.h" #include "common/cockpittest.h" #include "websocket/websocket.h" #include #include #include #include #include #define WAIT_UNTIL(cond) \ G_STMT_START \ while (!(cond)) g_main_context_iteration (NULL, TRUE); \ G_STMT_END typedef struct { CockpitTransport *transport; CockpitPipe *pipe; } TestCase; static void setup_with_child (TestCase *tc, gconstpointer data) { gchar *argv[] = { (gchar *)data, NULL }; GError *error = NULL; GPid pid; int in; int out; g_spawn_async_with_pipes (NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid, &in, &out, NULL, &error); g_assert_no_error (error); tc->pipe = g_object_new (COCKPIT_TYPE_PIPE, "name", "mock", "in-fd", out, "out-fd", in, "pid", pid, NULL); tc->transport = cockpit_pipe_transport_new (tc->pipe); } static void setup_no_child (TestCase *tc, gconstpointer data) { int sv[2]; if (socketpair (PF_LOCAL, SOCK_STREAM, 0, sv) < 0) g_assert_not_reached (); tc->pipe = cockpit_pipe_new ("mock", sv[0], sv[1]); tc->transport = cockpit_pipe_transport_new (tc->pipe); } static void teardown_transport (TestCase *tc, gconstpointer data) { cockpit_assert_expected (); g_object_add_weak_pointer (G_OBJECT (tc->transport), (gpointer *)&tc->transport); g_object_unref (tc->transport); /* If this asserts, outstanding references to transport */ g_assert (tc->transport == NULL); g_object_add_weak_pointer (G_OBJECT (tc->pipe), (gpointer *)&tc->pipe); g_object_unref (tc->pipe); /* If this asserts, outstanding references to transport */ g_assert (tc->pipe == NULL); } static gboolean on_recv_get_payload (CockpitTransport *transport, const gchar *channel, GBytes *message, gpointer user_data) { GBytes **received = user_data; if (channel == NULL) return FALSE; g_assert_cmpstr (channel, ==, "546"); g_assert (*received == NULL); *received = g_bytes_ref (message); return TRUE; } static gboolean on_recv_multiple (CockpitTransport *transport, const gchar *channel, GBytes *message, gpointer user_data) { gint *state = user_data; GBytes *check; if (channel == NULL) return FALSE; g_assert_cmpstr (channel, ==, "9"); if (*state == 0) check = g_bytes_new_static ("one", 3); else if (*state == 1) check = g_bytes_new_static ("two", 3); else g_assert_not_reached (); (*state)++; g_assert (g_bytes_equal (message, check)); g_bytes_unref (check); return TRUE; } static void on_closed_set_flag (CockpitTransport *transport, const gchar *problem, gpointer user_data) { gboolean *flag = user_data; g_assert (problem == NULL); g_assert (*flag == FALSE); *flag = TRUE; } static void test_properties (TestCase *tc, gconstpointer data) { CockpitPipe *pipe; g_assert (cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (tc->transport)) == tc->pipe); g_object_get (tc->transport, "pipe", &pipe, NULL); g_assert (pipe == tc->pipe); g_object_unref (pipe); } static void test_echo_and_close (TestCase *tc, gconstpointer data) { GBytes *received = NULL; GBytes *sent; gboolean closed = FALSE; sent = g_bytes_new_static ("the message", 11); g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received); cockpit_transport_send (tc->transport, "546", sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (received, sent)); g_bytes_unref (sent); g_bytes_unref (received); received = NULL; g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed); cockpit_transport_close (tc->transport, NULL); WAIT_UNTIL (closed == TRUE); } static void test_echo_queue (TestCase *tc, gconstpointer data) { GBytes *sent; gint state = 0; gboolean closed = FALSE; g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_multiple), &state); g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed); sent = g_bytes_new_static ("one", 3); cockpit_transport_send (tc->transport, "9", sent); g_bytes_unref (sent); sent = g_bytes_new_static ("two", 3); cockpit_transport_send (tc->transport, "9", sent); g_bytes_unref (sent); /* Only closes after above are sent */ cockpit_transport_close (tc->transport, NULL); WAIT_UNTIL (state == 2 && closed == TRUE); } static void test_echo_large (TestCase *tc, gconstpointer data) { GBytes *received = NULL; GBytes *sent; g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received); /* Medium length */ sent = g_bytes_new_take (g_strnfill (1020, '!'), 1020); cockpit_transport_send (tc->transport, "546", sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (received, sent)); g_bytes_unref (sent); g_bytes_unref (received); received = NULL; /* Extra large */ sent = g_bytes_new_take (g_strnfill (10 * 1000 * 1000, '?'), 10 * 1000 * 1000); cockpit_transport_send (tc->transport, "546", sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (received, sent)); g_bytes_unref (sent); g_bytes_unref (received); received = NULL; /* Double check that didn't csrew things up */ sent = g_bytes_new_static ("yello", 5); cockpit_transport_send (tc->transport, "546", sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (received, sent)); g_bytes_unref (sent); g_bytes_unref (received); received = NULL; } static void on_closed_get_problem (CockpitTransport *transport, const gchar *problem, gpointer user_data) { const gchar **ret = user_data; g_assert (problem != NULL); g_assert (*ret == NULL); *ret = g_strdup (problem); } static void test_close_problem (TestCase *tc, gconstpointer data) { gchar *problem = NULL; g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem); cockpit_transport_close (tc->transport, "right now"); WAIT_UNTIL (problem != NULL); g_assert_cmpstr (problem, ==, "right now"); g_free (problem); } static void test_terminate_problem (TestCase *tc, gconstpointer data) { gchar *problem = NULL; GPid pid; g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem); g_assert (cockpit_pipe_get_pid (tc->pipe, &pid)); g_assert (pid != 0); kill (pid, SIGTERM); WAIT_UNTIL (problem != NULL); g_assert_cmpstr (problem, ==, "terminated"); g_free (problem); } static void test_read_error (void) { CockpitTransport *transport; gchar *problem = NULL; gint fds[2]; /* Assuming FD 1000 is not taken */ g_assert (write (1000, "1", 1) < 0); g_assert_cmpint (pipe (fds), ==, 0); cockpit_expect_warning ("*Bad file descriptor"); cockpit_expect_warning ("*Bad file descriptor"); /* Pass in a bad read descriptor */ transport = cockpit_pipe_transport_new_fds ("test", 1000, fds[0]); g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem); WAIT_UNTIL (problem != NULL); g_assert_cmpstr (problem, ==, "internal-error"); g_free (problem); cockpit_assert_expected (); g_object_unref (transport); close (fds[1]); } static void test_write_error (void) { CockpitTransport *transport; gchar *problem = NULL; GBytes *sent; int fds[2]; /* Just used so we have a valid fd */ if (pipe(fds) < 0) g_assert_not_reached (); /* Assuming FD 1000 is not taken */ g_assert (write (1000, "1", 1) < 0); cockpit_expect_warning ("*Bad file descriptor"); cockpit_expect_warning ("*Bad file descriptor"); /* Pass in a bad write descriptor */ transport = cockpit_pipe_transport_new_fds ("test", fds[0], 1000); sent = g_bytes_new ("test", 4); cockpit_transport_send (transport, "3333", sent); g_bytes_unref (sent); g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem); WAIT_UNTIL (problem != NULL); g_assert_cmpstr (problem, ==, "internal-error"); g_free (problem); close (fds[0]); close (fds[1]); g_object_unref (transport); cockpit_assert_expected (); } static void test_read_combined (void) { CockpitTransport *transport; struct iovec iov[4]; gint state = 0; gint fds[2]; gint out; if (pipe(fds) < 0) g_assert_not_reached (); out = dup (2); g_assert (out >= 0); /* Pass in a read end of the pipe */ transport = cockpit_pipe_transport_new_fds ("test", fds[0], out); g_signal_connect (transport, "recv", G_CALLBACK (on_recv_multiple), &state); /* Write two messages to the pipe at once */ iov[0].iov_base = "5\n"; iov[0].iov_len = 2; iov[1].iov_base = "9\none"; iov[1].iov_len = 5; iov[2].iov_base = "5\n"; iov[2].iov_len = 2; iov[3].iov_base = "9\ntwo"; iov[3].iov_len = 5; g_assert_cmpint (writev (fds[1], iov, 4), ==, 14); WAIT_UNTIL (state == 2); close (fds[1]); g_object_unref (transport); } static void test_read_truncated (void) { CockpitTransport *transport; gchar *problem = NULL; gint fds[2]; gint out; if (pipe(fds) < 0) g_assert_not_reached (); out = dup (2); g_assert (out >= 0); /* Pass in a read end of the pipe */ transport = cockpit_pipe_transport_new_fds ("test", fds[0], out); g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem); /* Not a full 4 byte length (ie: truncated) */ g_assert_cmpint (write (fds[1], "5", 1), ==, 1); g_assert_cmpint (close (fds[1]), ==, 0); WAIT_UNTIL (problem != NULL); g_assert_cmpstr (problem, ==, "disconnected"); g_free (problem); g_object_unref (transport); cockpit_assert_expected (); } static void test_incorrect_protocol (void) { CockpitTransport *transport; gchar *problem = NULL; gint fds[2]; gint out; if (pipe(fds) < 0) g_assert_not_reached (); out = dup (2); g_assert (out >= 0); cockpit_expect_warning ("*received invalid length prefix"); /* Pass in a read end of the pipe */ transport = cockpit_pipe_transport_new_fds ("test", fds[0], out); g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem); /* Not a full 4 byte length (ie: truncated) */ g_assert_cmpint (write (fds[1], "X", 1), ==, 1); g_assert_cmpint (close (fds[1]), ==, 0); WAIT_UNTIL (problem != NULL); g_assert_cmpstr (problem, ==, "protocol-error"); g_free (problem); g_object_unref (transport); cockpit_assert_expected (); } static void test_parse_frame (void) { GBytes *message; GBytes *payload; gchar *channel; message = g_bytes_new_static ("134\ntest", 8); payload = cockpit_transport_parse_frame (message, &channel); g_assert (payload != NULL); g_assert_cmpstr (g_bytes_get_data (payload, NULL), ==, "test"); g_assert_cmpstr (channel, ==, "134"); g_bytes_unref (payload); g_bytes_unref (message); g_free (channel); } static void test_parse_frame_bad (void) { gchar *channel = NULL; GBytes *message; GBytes *payload; cockpit_expect_message ("*invalid channel prefix"); message = g_bytes_new_static ("b\x00y\ntest", 8); payload = cockpit_transport_parse_frame (message, &channel); g_assert (payload == NULL); g_bytes_unref (message); g_free (channel); cockpit_assert_expected (); cockpit_expect_message ("*invalid message without channel prefix"); channel = NULL; message = g_bytes_new_static ("test", 4); payload = cockpit_transport_parse_frame (message, &channel); g_assert (payload == NULL); g_bytes_unref (message); g_free (channel); cockpit_assert_expected (); } static void test_parse_frame_maybe (void) { gchar *channel = NULL; GBytes *message; GBytes *payload; message = g_bytes_new_static ("b\x00y\ntest", 8); payload = cockpit_transport_maybe_frame (message, &channel); g_assert (payload == NULL); g_bytes_unref (message); g_free (channel); channel = NULL; message = g_bytes_new_static ("test", 4); payload = cockpit_transport_maybe_frame (message, &channel); g_assert (payload == NULL); g_bytes_unref (message); g_free (channel); } static void test_parse_command (void) { const gchar *input = "{ \"command\": \"test\", \"channel\": \"66\", \"opt\": \"one\" }"; GBytes *message; const gchar *channel; const gchar *command; JsonObject *options; gboolean ret; message = g_bytes_new_static (input, strlen (input)); ret = cockpit_transport_parse_command (message, &command, &channel, &options); g_bytes_unref (message); g_assert (ret == TRUE); g_assert_cmpstr (command, ==, "test"); g_assert_cmpstr (channel, ==, "66"); g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one"); json_object_unref (options); } static void test_parse_command_no_channel (void) { const gchar *input = "{ \"command\": \"test\", \"opt\": \"one\" }"; GBytes *message; const gchar *channel; const gchar *command; JsonObject *options; gboolean ret; message = g_bytes_new_static (input, strlen (input)); ret = cockpit_transport_parse_command (message, &command, &channel, &options); g_bytes_unref (message); g_assert (ret == TRUE); g_assert_cmpstr (command, ==, "test"); g_assert_cmpstr (channel, ==, NULL); g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one"); json_object_unref (options); } static void test_parse_command_nulls (void) { const gchar *input = "{ \"command\": \"test\", \"opt\": \"one\" }"; GBytes *message; JsonObject *options; gboolean ret; message = g_bytes_new_static (input, strlen (input)); ret = cockpit_transport_parse_command (message, NULL, NULL, &options); g_bytes_unref (message); g_assert (ret == TRUE); g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one"); json_object_unref (options); } struct { const char *name; const char *json; } bad_command_payloads[] = { { "no-command", "{ \"no-command\": \"test\" }", }, { "empty-command", "{ \"command\": \"\" }", }, { "invalid-json", "{ xxxxxxxxxxxxxxxxxxxxx", }, { "not-an-object", "55", }, { "number-channel", "{ \"command\": \"test\", \"channel\": 0 }", }, { "empty-channel", "{ \"command\": \"test\", \"channel\": \"\" }", }, { "newline-channel", "{ \"command\": \"test\", \"channel\": \"blah\nline\" }", }, }; static void test_parse_command_bad (gconstpointer input) { GBytes *message; const gchar *channel; const gchar *command; JsonObject *options; gboolean ret; cockpit_expect_warning ("*"); message = g_bytes_new_static (input, strlen (input)); ret = cockpit_transport_parse_command (message, &command, &channel, &options); g_bytes_unref (message); g_assert (ret == FALSE); cockpit_assert_expected (); } int main (int argc, char *argv[]) { gint i; cockpit_test_init (&argc, &argv); g_test_add_func ("/transport/parse-frame/ok", test_parse_frame); g_test_add_func ("/transport/parse-frame/bad", test_parse_frame_bad); g_test_add_func ("/transport/parse-frame/maybe", test_parse_frame_maybe); g_test_add_func ("/transport/parse-command/normal", test_parse_command); g_test_add_func ("/transport/parse-command/no-channel", test_parse_command_no_channel); g_test_add_func ("/transport/parse-command/nulls", test_parse_command_nulls); for (i = 0; i < G_N_ELEMENTS (bad_command_payloads); i++) { gchar *name = g_strdup_printf ("/transport/parse-command/%s", bad_command_payloads[i].name); g_test_add_data_func (name, bad_command_payloads[i].json, test_parse_command_bad); g_free (name); } g_test_add ("/transport/properties", TestCase, NULL, setup_no_child, test_properties, teardown_transport); g_test_add ("/transport/echo-message/child", TestCase, BUILDDIR "/mock-echo", setup_with_child, test_echo_and_close, teardown_transport); g_test_add ("/transport/echo-message/no-child", TestCase, NULL, setup_no_child, test_echo_and_close, teardown_transport); g_test_add ("/transport/echo-queue/child", TestCase, BUILDDIR "/mock-echo", setup_with_child, test_echo_queue, teardown_transport); g_test_add ("/transport/echo-queue/no-child", TestCase, NULL, setup_no_child, test_echo_queue, teardown_transport); g_test_add ("/transport/echo-large/child", TestCase, "cat", setup_with_child, test_echo_large, teardown_transport); g_test_add ("/transport/echo-large/no-child", TestCase, NULL, setup_no_child, test_echo_large, teardown_transport); g_test_add ("/transport/close-problem/child", TestCase, BUILDDIR "/mock-echo", setup_with_child, test_close_problem, teardown_transport); g_test_add ("/transport/close-problem/no-child", TestCase, NULL, setup_no_child, test_close_problem, teardown_transport); g_test_add ("/transport/terminate-problem", TestCase, BUILDDIR "/mock-echo", setup_with_child, test_terminate_problem, teardown_transport); g_test_add_func ("/transport/read-error", test_read_error); g_test_add_func ("/transport/write-error", test_write_error); g_test_add_func ("/transport/read-combined", test_read_combined); g_test_add_func ("/transport/read-truncated", test_read_truncated); g_test_add_func ("/transport/read-incorrect", test_incorrect_protocol); return g_test_run (); }