/*
* This file is part of Cockpit.
*
* Copyright (C) 2013 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 "cockpitpipechannel.h"
#include "mock-transport.h"
#include "common/cockpitjson.h"
#include "common/cockpittest.h"
#include
#include
#include
#include
#include
#include
#include
/* -----------------------------------------------------------------------------
* Test
*/
typedef struct {
GSocket *listen_sock;
GSource *listen_source;
GSocket *conn_sock;
GSource *conn_source;
MockTransport *transport;
CockpitChannel *channel;
gchar *channel_problem;
const gchar *unix_path;
gchar *temp_file;
} TestCase;
static gboolean
on_socket_input (GSocket *socket,
GIOCondition cond,
gpointer user_data)
{
gchar buffer[1024];
GError *error = NULL;
gssize ret, wret;
ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
g_assert_no_error (error);
if (ret == 0)
{
g_socket_shutdown (socket, FALSE, TRUE, &error);
g_assert_no_error (error);
return FALSE;
}
g_assert (ret > 0);
wret = g_socket_send (socket, buffer, ret, NULL, &error);
g_assert_no_error (error);
g_assert (wret == ret);
return TRUE;
}
static gboolean
on_socket_connection (GSocket *socket,
GIOCondition cond,
gpointer user_data)
{
TestCase *tc = user_data;
GError *error = NULL;
g_assert (tc->conn_source == NULL);
tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
g_assert_no_error (error);
tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
g_source_attach (tc->conn_source, NULL);
/* Only one connection */
return FALSE;
}
static void
setup (TestCase *tc,
gconstpointer data)
{
GSocketAddress *address;
GError *error = NULL;
tc->unix_path = data;
if (tc->unix_path == NULL)
{
tc->unix_path = tc->temp_file = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
g_assert (close (g_mkstemp (tc->temp_file)) == 0);
g_assert (g_unlink (tc->temp_file) == 0);
}
address = g_unix_socket_address_new (tc->unix_path);
tc->listen_sock = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_STREAM,
G_SOCKET_PROTOCOL_DEFAULT, &error);
g_assert_no_error (error);
g_socket_bind (tc->listen_sock, address, TRUE, &error);
g_assert_no_error (error);
g_socket_listen (tc->listen_sock, &error);
g_assert_no_error (error);
tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
g_source_attach (tc->listen_source, NULL);
g_object_unref (address);
tc->transport = g_object_new (mock_transport_get_type (), NULL);
}
static void
on_closed_get_problem (CockpitChannel *channel,
const gchar *problem,
gpointer user_data)
{
gchar **retval = user_data;
g_assert (retval != NULL && *retval == NULL);
*retval = g_strdup (problem ? problem : "");
}
static void
setup_channel (TestCase *tc,
gconstpointer data)
{
setup (tc, data);
tc->channel = cockpit_pipe_channel_open (COCKPIT_TRANSPORT (tc->transport), "548", tc->unix_path);
g_signal_connect (tc->channel, "closed", G_CALLBACK (on_closed_get_problem), &tc->channel_problem);
}
static void
teardown (TestCase *tc,
gconstpointer data)
{
if (tc->conn_source)
{
g_source_destroy (tc->conn_source);
g_source_unref (tc->conn_source);
}
if (tc->listen_source)
{
g_source_destroy (tc->listen_source);
g_source_unref (tc->listen_source);
}
g_clear_object (&tc->listen_sock);
g_clear_object (&tc->conn_sock);
g_unlink (tc->unix_path);
g_free (tc->temp_file);
g_object_unref (tc->transport);
if (tc->channel)
{
g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
g_object_unref (tc->channel);
g_assert (tc->channel == NULL);
}
g_free (tc->channel_problem);
cockpit_assert_expected ();
}
static void
expect_control_message (JsonObject *options,
const gchar *command,
const gchar *expected_channel,
...) G_GNUC_NULL_TERMINATED;
static void
expect_control_message (JsonObject *options,
const gchar *expected_command,
const gchar *expected_channel,
...)
{
const gchar *expect_option;
const gchar *expect_value;
const gchar *value;
JsonNode *node;
va_list va;
g_assert (options != NULL);
g_assert_cmpstr (json_object_get_string_member (options, "command"), ==, expected_command);
g_assert_cmpstr (json_object_get_string_member (options, "channel"), ==, expected_channel);
va_start (va, expected_channel);
for (;;) {
expect_option = va_arg (va, const gchar *);
if (!expect_option)
break;
expect_value = va_arg (va, const gchar *);
value = NULL;
node = json_object_get_member (options, expect_option);
if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
value = json_node_get_string (node);
g_assert_cmpstr (value, ==, expect_value);
}
va_end (va);
}
static void
test_echo (TestCase *tc,
gconstpointer unused)
{
GBytes *payload;
GBytes *sent;
payload = g_bytes_new ("Marmalaade!", 11);
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
while (mock_transport_count_sent (tc->transport) < 2)
g_main_context_iteration (NULL, TRUE);
sent = mock_transport_pop_channel (tc->transport, "548");
cockpit_assert_bytes_eq (sent, "Marmalaade!", 11);
g_bytes_unref (payload);
}
static void
test_shutdown (TestCase *tc,
gconstpointer unused)
{
GError *error = NULL;
JsonObject *sent;
/* Wait until the socket has opened */
while (tc->conn_sock == NULL)
g_main_context_iteration (NULL, TRUE);
/*
* Close down the write end of the socket (what
* CockpitTextStream is reading from)
*/
g_socket_shutdown (tc->conn_sock, FALSE, TRUE, &error);
g_assert_no_error (error);
while (tc->channel_problem == NULL)
g_main_context_iteration (NULL, TRUE);
g_assert_cmpstr (tc->channel_problem, ==, "");
sent = mock_transport_pop_control (tc->transport);
expect_control_message (sent, "ready", "548", NULL);
sent = mock_transport_pop_control (tc->transport);
expect_control_message (sent, "done", "548", NULL);
sent = mock_transport_pop_control (tc->transport);
expect_control_message (sent, "close", "548", "problem", NULL, NULL);
}
static void
test_close_normal (TestCase *tc,
gconstpointer unused)
{
GBytes *payload;
GBytes *sent;
JsonObject *control;
/* Wait until the socket has opened */
while (tc->conn_sock == NULL)
g_main_context_iteration (NULL, TRUE);
payload = g_bytes_new ("Marmalaade!", 11);
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
cockpit_channel_close (tc->channel, NULL);
/* Wait until channel closes */
while (tc->channel_problem == NULL)
g_main_context_iteration (NULL, TRUE);
/* Should have sent payload and control */
g_assert_cmpstr (tc->channel_problem, ==, "");
sent = mock_transport_pop_channel (tc->transport, "548");
g_assert (sent != NULL);
g_assert (g_bytes_equal (sent, payload));
g_bytes_unref (payload);
control = mock_transport_pop_control (tc->transport);
expect_control_message (control, "ready", "548", NULL);
control = mock_transport_pop_control (tc->transport);
expect_control_message (control, "done", "548", NULL);
control = mock_transport_pop_control (tc->transport);
expect_control_message (control, "close", "548", "problem", NULL, NULL);
}
static void
test_close_problem (TestCase *tc,
gconstpointer unused)
{
GBytes *sent;
/* Wait until the socket has opened */
while (tc->conn_sock == NULL)
g_main_context_iteration (NULL, TRUE);
sent = g_bytes_new ("Marmalaade!", 11);
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
g_bytes_unref (sent);
cockpit_channel_close (tc->channel, "boooyah");
/* Wait until channel closes */
while (tc->channel_problem == NULL)
g_main_context_iteration (NULL, TRUE);
/* Should have sent no payload and control */
g_assert_cmpstr (tc->channel_problem, ==, "boooyah");
g_assert (mock_transport_pop_channel (tc->transport, "548") == NULL);
expect_control_message (mock_transport_pop_control (tc->transport), "ready", "548", NULL);
expect_control_message (mock_transport_pop_control (tc->transport),
"close", "548", "problem", "boooyah", NULL);
}
static void
test_spawn_simple (void)
{
MockTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
JsonObject *options;
JsonArray *array;
GBytes *sent;
transport = g_object_new (mock_transport_get_type (), NULL);
options = json_object_new ();
array = json_array_new ();
json_array_add_string_element (array, "/bin/cat");
json_object_set_array_member (options, "spawn", array);
json_object_set_string_member (options, "payload", "stream");
channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
"options", options,
"id", "548",
"transport", transport,
NULL);
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
while (g_main_context_iteration (NULL, FALSE));
json_object_unref (options);
sent = g_bytes_new ("Marmalaade!", 11);
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
cockpit_channel_close (channel, NULL);
while (mock_transport_count_sent (transport) < 2)
g_main_context_iteration (NULL, TRUE);
g_assert (g_bytes_equal (sent, mock_transport_pop_channel (transport, "548")));
g_bytes_unref (sent);
while (!problem)
g_main_context_iteration (NULL, TRUE);
g_assert_cmpstr (problem, ==, "");
g_object_unref (channel);
g_free (problem);
g_object_unref (transport);
}
static void
test_spawn_environ (void)
{
MockTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
JsonObject *options;
JsonArray *array;
GString *string;
gconstpointer data;
gsize len;
GBytes *sent;
transport = g_object_new (mock_transport_get_type (), NULL);
options = json_object_new ();
array = json_array_new ();
json_array_add_string_element (array, "/bin/sh");
json_array_add_string_element (array, "-c");
json_array_add_string_element (array, "set");
json_object_set_array_member (options, "spawn", array);
array = json_array_new ();
json_array_add_string_element (array, "ENVIRON=Marmalaade");
json_object_set_array_member (options, "environ", array);
json_object_set_string_member (options, "payload", "stream");
channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
"options", options,
"id", "548",
"transport", transport,
NULL);
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
json_object_unref (options);
string = g_string_new ("");
while (!problem)
{
g_main_context_iteration (NULL, TRUE);
sent = mock_transport_pop_channel (transport, "548");
if (sent)
{
data = g_bytes_get_data (sent, &len);
g_string_append_len (string, data, len);
}
}
g_assert_cmpstr (problem, ==, "");
g_free (problem);
cockpit_assert_strmatch (string->str, "*ENVIRON=*Marmalaade*");
g_string_free (string, TRUE);
g_object_unref (channel);
g_object_unref (transport);
}
static void
test_spawn_status (void)
{
MockTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
JsonObject *options;
JsonArray *array;
JsonObject *control;
transport = g_object_new (mock_transport_get_type (), NULL);
options = json_object_new ();
array = json_array_new ();
json_array_add_string_element (array, "/bin/sh");
json_array_add_string_element (array, "-c");
json_array_add_string_element (array, "exit 5");
json_object_set_array_member (options, "spawn", array);
json_object_set_string_member (options, "payload", "stream");
channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
"options", options,
"id", "548",
"transport", transport,
NULL);
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
json_object_unref (options);
while (!problem)
g_main_context_iteration (NULL, TRUE);
control = mock_transport_pop_control (transport);
expect_control_message (control, "ready", "548", NULL);
control = mock_transport_pop_control (transport);
expect_control_message (control, "done", "548", NULL);
control = mock_transport_pop_control (transport);
expect_control_message (control, "close", "548", "problem", NULL, NULL);
g_assert_cmpint (json_object_get_int_member (control, "exit-status"), ==, 5);
g_free (problem);
g_object_unref (channel);
g_object_unref (transport);
}
static void
test_spawn_signal (void)
{
MockTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
JsonObject *options;
JsonArray *array;
JsonObject *control;
transport = g_object_new (mock_transport_get_type (), NULL);
options = json_object_new ();
array = json_array_new ();
json_array_add_string_element (array, "/bin/sh");
json_array_add_string_element (array, "-c");
json_array_add_string_element (array, "kill $$");
json_object_set_array_member (options, "spawn", array);
json_object_set_string_member (options, "payload", "stream");
channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
"options", options,
"id", "548",
"transport", transport,
NULL);
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
json_object_unref (options);
while (!problem)
g_main_context_iteration (NULL, TRUE);
control = mock_transport_pop_control (transport);
expect_control_message (control, "ready", "548", NULL);
control = mock_transport_pop_control (transport);
expect_control_message (control, "done", "548", NULL);
control = mock_transport_pop_control (transport);
cockpit_assert_json_eq (control, "{ \"command\": \"close\", \"channel\": \"548\","
" \"exit-signal\": \"TERM\"}");
g_free (problem);
g_object_unref (channel);
g_object_unref (transport);
}
static void
test_spawn_pty (void)
{
MockTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
JsonObject *options;
JsonArray *array;
GBytes *sent;
GString *received;
gconstpointer data;
gsize len;
transport = g_object_new (mock_transport_get_type (), NULL);
options = json_object_new ();
array = json_array_new ();
json_array_add_string_element (array, "/bin/bash");
json_array_add_string_element (array, "-i");
json_object_set_array_member (options, "spawn", array);
json_object_set_string_member (options, "payload", "stream");
json_object_set_boolean_member (options, "pty", TRUE);
channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
"options", options,
"id", "548",
"transport", transport,
NULL);
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
json_object_unref (options);
sent = g_bytes_new ("echo booyah\nexit\n", 17);
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
g_bytes_unref (sent);
received = g_string_new ("");
while (!problem)
{
g_main_context_iteration (NULL, TRUE);
sent = mock_transport_pop_channel (transport, "548");
if (sent)
{
data = g_bytes_get_data (sent, &len);
g_string_append_len (received, data, len);
}
}
cockpit_assert_strmatch (received->str, "*booyah*");
g_string_free (received, TRUE);
g_assert_cmpstr (problem, ==, "");
g_object_unref (channel);
g_free (problem);
g_object_unref (transport);
}
/* Create GBytes with the contents of @s, without terminating \0 */
static GBytes *
bytes_from_string (const gchar *s)
{
gsize len;
len = strlen (s);
return g_bytes_new (s, len);
}
static void
test_spawn_pty_resize (void)
{
MockTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
JsonObject *options;
JsonArray *array;
JsonObject *window;
GBytes *sent;
GString *received;
gconstpointer data;
gsize len;
transport = g_object_new (mock_transport_get_type (), NULL);
options = json_object_new ();
array = json_array_new ();
json_array_add_string_element (array, "/bin/bash");
json_array_add_string_element (array, "-i");
json_object_set_array_member (options, "spawn", array);
json_object_set_string_member (options, "payload", "stream");
json_object_set_boolean_member (options, "pty", TRUE);
window = json_object_new ();
json_object_set_int_member (window, "rows", 1234);
json_object_set_int_member (window, "cols", 4567);
json_object_set_object_member (options, "window", window);
channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
"options", options,
"id", "548",
"transport", transport,
NULL);
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
json_object_unref (options);
sent = bytes_from_string ("echo -e \"\\x7b$COLUMNS $LINES\\x7d\"\n");
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
g_bytes_unref (sent);
received = g_string_new ("");
while (!problem)
{
g_main_context_iteration (NULL, TRUE);
sent = mock_transport_pop_channel (transport, "548");
if (sent)
{
data = g_bytes_get_data (sent, &len);
g_string_append_len (received, data, len);
if (memchr (data, '}', len))
break;
}
}
options = json_object_new ();
window = json_object_new ();
json_object_set_int_member (window, "rows", 24);
json_object_set_int_member (window, "cols", 42);
json_object_set_object_member (options, "window", window);
cockpit_transport_emit_control (COCKPIT_TRANSPORT (transport), "options", "548", options, NULL);
json_object_unref (options);
sent = bytes_from_string ("echo -e \"\\x7b$COLUMNS $LINES\\x7d\"\nexit\n");
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
g_bytes_unref (sent);
while (!problem)
{
g_main_context_iteration (NULL, TRUE);
sent = mock_transport_pop_channel (transport, "548");
if (sent)
{
data = g_bytes_get_data (sent, &len);
g_string_append_len (received, data, len);
}
}
cockpit_assert_strmatch (received->str, "*{4567 1234}*{42 24}*");
g_string_free (received, TRUE);
g_assert_cmpstr (problem, ==, "");
g_object_unref (channel);
g_free (problem);
g_object_unref (transport);
}
static void
test_send_invalid (TestCase *tc,
gconstpointer unused)
{
GBytes *converted;
GBytes *sent;
sent = g_bytes_new ("Oh \x00Marma\x00laade!", 16);
cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
g_bytes_unref (sent);
while (mock_transport_count_sent (tc->transport) < 2)
g_main_context_iteration (NULL, TRUE);
converted = g_bytes_new ("Oh \xef\xbf\xbd""Marma""\xef\xbf\xbd""laade!", 20);
g_assert (g_bytes_equal (converted, mock_transport_pop_channel (tc->transport, "548")));
g_bytes_unref (converted);
}
static void
test_recv_invalid (TestCase *tc,
gconstpointer unused)
{
GError *error = NULL;
GBytes *converted;
/* Wait until the socket has opened */
while (tc->conn_sock == NULL)
g_main_context_iteration (NULL, TRUE);
g_assert_cmpint (g_socket_send (tc->conn_sock, "\x00Marmalaade!\x00", 13, NULL, &error), ==, 13);
g_assert_no_error (error);
while (mock_transport_count_sent (tc->transport) < 2)
g_main_context_iteration (NULL, TRUE);
converted = g_bytes_new ("\xef\xbf\xbd""Marmalaade!""\xef\xbf\xbd", 17);
g_assert (g_bytes_equal (converted, mock_transport_pop_channel (tc->transport, "548")));
g_bytes_unref (converted);
}
static void
test_fail_not_found (void)
{
CockpitTransport *transport;
CockpitChannel *channel;
gchar *problem = NULL;
cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "*couldn't connect*");
transport = g_object_new (mock_transport_get_type (), NULL);
channel = cockpit_pipe_channel_open (transport, "1", "/non-existent");
g_assert (channel != NULL);
/* Even through failure is on open, should not have closed yet */
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
while (problem == NULL)
g_main_context_iteration (NULL, TRUE);
g_assert_cmpstr (problem, ==, "not-found");
g_free (problem);
g_object_unref (channel);
g_object_unref (transport);
cockpit_assert_expected ();
}
static void
test_fail_access_denied (void)
{
CockpitTransport *transport;
CockpitChannel *channel;
gchar *unix_path;
gchar *problem = NULL;
gint fd;
if (geteuid () == 0)
{
cockpit_test_skip ("running as root");
return;
}
cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "*couldn't connect*");
unix_path = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
fd = g_mkstemp (unix_path);
g_assert_cmpint (fd, >=, 0);
/* Take away all permissions from the file */
g_assert_cmpint (fchmod (fd, 0000), ==, 0);
transport = g_object_new (mock_transport_get_type (), NULL);
channel = cockpit_pipe_channel_open (transport, "1", unix_path);
g_assert (channel != NULL);
/* Even through failure is on open, should not have closed yet */
g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
while (problem == NULL)
g_main_context_iteration (NULL, TRUE);
g_assert_cmpstr (problem, ==, "access-denied");
g_free (problem);
g_free (unix_path);
close (fd);
g_object_unref (channel);
g_object_unref (transport);
cockpit_assert_expected ();
}
int
main (int argc,
char *argv[])
{
cockpit_test_init (&argc, &argv);
g_test_add ("/pipe-channel/echo", TestCase, NULL,
setup_channel, test_echo, teardown);
g_test_add ("/pipe-channel/shutdown", TestCase, NULL,
setup_channel, test_shutdown, teardown);
g_test_add ("/pipe-channel/close-normal", TestCase, NULL,
setup_channel, test_close_normal, teardown);
g_test_add ("/pipe-channel/close-problem", TestCase, NULL,
setup_channel, test_close_problem, teardown);
g_test_add ("/pipe-channel/invalid-send", TestCase, NULL,
setup_channel, test_send_invalid, teardown);
g_test_add ("/pipe-channel/invalid-recv", TestCase, NULL,
setup_channel, test_recv_invalid, teardown);
g_test_add_func ("/pipe-channel/spawn/signal", test_spawn_signal);
g_test_add_func ("/pipe-channel/spawn/simple", test_spawn_simple);
g_test_add_func ("/pipe-channel/spawn/status", test_spawn_status);
g_test_add_func ("/pipe-channel/spawn/environ", test_spawn_environ);
g_test_add_func ("/pipe-channel/spawn/pty", test_spawn_pty);
g_test_add_func ("/pipe-channel/spawn/pty-resize", test_spawn_pty_resize);
g_test_add_func ("/pipe-channel/fail/not-found", test_fail_not_found);
g_test_add_func ("/pipe-channel/fail/access-denied", test_fail_access_denied);
return g_test_run ();
}