/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 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 "common/cockpitauthorize.h"
#include "common/cockpittest.h"
#include "common/cockpiterror.h"
#include "common/cockpitpipe.h"
#include "common/cockpitpipetransport.h"
#include "common/cockpitjson.h"
#include
#include
#include
#include
#include
#include
#define TIMEOUT 30
#define WAIT_UNTIL(cond) \
G_STMT_START \
while (!(cond)) g_main_context_iteration (NULL, TRUE); \
G_STMT_END
#define PASSWORD "this is the password"
typedef struct {
CockpitTransport *transport;
gboolean closed;
/* setup_mock_sshd */
GPid mock_sshd;
guint16 ssh_port;
} TestCase;
typedef struct {
const char *ssh_command;
const char *mock_sshd_arg;
const char *client_password;
const char *username;
const char *knownhosts_data;
const char *knownhosts_file;
const char *config;
const char *problem;
gboolean allow_unknown;
} TestFixture;
static GString *
read_all_into_string (int fd)
{
GString *input = g_string_new ("");
gsize len;
gssize ret;
for (;;)
{
len = input->len;
g_string_set_size (input, len + 256);
ret = read (fd, input->str + len, 256);
if (ret < 0)
{
if (errno != EAGAIN)
{
g_critical ("couldn't read from mock input: %s", g_strerror (errno));
g_string_free (input, TRUE);
return NULL;
}
}
else if (ret == 0)
{
return input;
}
else
{
input->len = len + ret;
input->str[input->len] = '\0';
}
}
}
static void
spawn_setup (gpointer data)
{
int fd = GPOINTER_TO_INT (data);
/* Send this signal to all direct child processes, when bridge dies */
prctl (PR_SET_PDEATHSIG, SIGHUP);
g_assert_cmpint (dup2 (fd, 0), >, -1);
g_assert_cmpint (dup2 (fd, 1), >, -1);
close (fd);
}
static void
setup_mock_sshd (TestCase *tc,
gconstpointer data)
{
const TestFixture *fixture = data;
GError *error = NULL;
GString *port;
gchar *endptr;
guint64 value;
gint out_fd;
const gchar *argv[] = {
BUILDDIR "/mock-sshd",
"--user", g_get_user_name (),
"--password", PASSWORD,
fixture->mock_sshd_arg,
NULL
};
g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
&tc->mock_sshd, NULL, &out_fd, NULL, &error);
g_assert_no_error (error);
/*
* mock-sshd prints its port on stdout, and then closes stdout
* This also lets us know when it has initialized.
*/
port = read_all_into_string (out_fd);
g_assert (port != NULL);
close (out_fd);
g_assert_no_error (error);
g_strstrip (port->str);
value = g_ascii_strtoull (port->str, &endptr, 10);
if (!endptr || *endptr != '\0' || value == 0 || value > G_MAXUSHORT)
g_critical ("invalid port printed by mock-sshd: %s", port->str);
tc->ssh_port = (gushort)value;
g_string_free (port, TRUE);
}
static const TestFixture fixture_mock_echo = {
.ssh_command = BUILDDIR "/mock-echo"
};
static const TestFixture fixture_cat = {
.ssh_command = SRCDIR "/src/ws/mock-cat-with-init"
};
static gchar **
setup_env (const TestFixture *fix)
{
const gchar *command;
const gchar *knownhosts_file;
const gchar *config;
gchar **env = g_get_environ ();
config = fix ? fix->config : NULL;
if (!config)
config = SRCDIR "/src/ssh/mock-config";
env = g_environ_setenv (env, "XDG_CONFIG_DIRS", config, TRUE);
command = fix ? fix->ssh_command : NULL;
if (!command)
command = fixture_cat.ssh_command;
env = g_environ_setenv (env, "COCKPIT_SSH_BRIDGE_COMMAND", command, TRUE);
if (fix && fix->allow_unknown)
{
env = g_environ_setenv (env, "COCKPIT_SSH_ALLOW_UNKNOWN",
"true", TRUE);
}
knownhosts_file = fix ? fix->knownhosts_file : NULL;
if (!knownhosts_file)
knownhosts_file = SRCDIR "/src/ssh/mock_known_hosts";
env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE",
knownhosts_file, TRUE);
return env;
}
static CockpitTransport *
start_bridge (gchar **env,
gchar **argv)
{
GError *error = NULL;
int fds[2];
g_assert_cmpint (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds), ==, 0);
g_spawn_async_with_pipes (BUILDDIR, argv, env,
G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH,
spawn_setup, GINT_TO_POINTER (fds[0]),
NULL, NULL, NULL, NULL, &error);
g_assert_no_error (error);
close (fds[0]);
return cockpit_pipe_transport_new_fds ("test-ssh", fds[1], fds[1]);
}
static void
on_closed_set_flag (CockpitTransport *transport,
const gchar *problem,
gpointer user_data)
{
gboolean *flag = user_data;
g_assert_cmpstr (problem, ==, NULL);
g_assert (*flag == FALSE);
*flag = TRUE;
}
static void
setup (TestCase *tc,
gconstpointer data)
{
const TestFixture *fixture = data;
const gchar *argv[] = { BUILDDIR "/cockpit-ssh", NULL, NULL };
gchar **env = NULL;
gchar *host = NULL;
gchar *knownhosts_data = NULL;
alarm (TIMEOUT);
g_assert (fixture != NULL);
env = setup_env (fixture);
setup_mock_sshd (tc, data);
if (tc->ssh_port)
host = g_strdup_printf ("127.0.0.1:%d", tc->ssh_port);
else
host = g_strdup ("127.0.0.1");
argv[1] = host;
if (fixture && fixture->knownhosts_data)
{
if (fixture->knownhosts_data[0] != '\0' &&
fixture->knownhosts_data[0] != '*' &&
!g_str_equal (fixture->knownhosts_data, "authorize"))
{
knownhosts_data = g_strdup_printf ("[127.0.0.1]:%d %s",
tc->ssh_port ? (int)tc->ssh_port : 22,
fixture->knownhosts_data);
}
else
{
knownhosts_data = g_strdup (fixture->knownhosts_data);
}
env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_DATA",
knownhosts_data, TRUE);
}
tc->transport = start_bridge (env, (gchar **) argv);
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &tc->closed);
g_strfreev (env);
g_free (host);
g_free (knownhosts_data);
}
static void
teardown (TestCase *tc,
gconstpointer data)
{
WAIT_UNTIL (tc->closed == TRUE);
g_object_add_weak_pointer (G_OBJECT (tc->transport), (gpointer*)&tc->transport);
g_object_unref (tc->transport);
/* If this asserts, outstanding references */
g_assert (tc->transport == NULL);
if (tc->mock_sshd)
{
kill (tc->mock_sshd, SIGTERM);
g_assert_cmpint (waitpid (tc->mock_sshd, 0, 0), ==, tc->mock_sshd);
g_spawn_close_pid (tc->mock_sshd);
}
alarm (0);
}
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 gboolean
on_control_get_options (CockpitTransport *transport,
const gchar *command,
const gchar *channel,
JsonObject *options,
GBytes *payload,
gpointer user_data)
{
JsonObject **ret_options = user_data;
g_assert (ret_options);
g_assert (*ret_options == NULL);
*ret_options = json_object_ref (options);
return TRUE;
}
static void
do_auth_response (CockpitTransport *transport,
const gchar *challenge,
const gchar *response)
{
JsonObject *auth = NULL;
GBytes *payload = NULL;
const gchar *cookie;
guint sig = 0;
sig = g_signal_connect (transport, "control",
G_CALLBACK (on_control_get_options),
&auth);
WAIT_UNTIL (auth != NULL);
g_signal_handler_disconnect (transport, sig);
g_assert (cockpit_json_get_string (auth, "cookie", NULL, &cookie));
g_assert_cmpstr (json_object_get_string_member (auth, "command"),
==, "authorize");
g_assert_cmpstr (json_object_get_string_member (auth, "challenge"),
==, challenge);
g_assert_cmpstr (cookie, !=, NULL);
payload = cockpit_transport_build_control ("command", "authorize",
"cookie", cookie,
"response", response,
NULL);
cockpit_transport_send (transport, NULL, payload);
g_bytes_unref (payload);
json_object_unref (auth);
}
static void
do_basic_auth (CockpitTransport *transport,
const gchar *challange,
const gchar *user,
const gchar *password)
{
gchar *userpass = NULL;
gchar *encoded = NULL;
gchar *response = NULL;
userpass = g_strdup_printf ("%s:%s", user, password);
encoded = g_base64_encode ((guchar *)userpass, strlen (userpass));
response = g_strdup_printf ("Basic %s", encoded);
do_auth_response (transport, challange, response);
g_free (userpass);
g_free (response);
g_free (encoded);
}
static void
do_fixture_auth (CockpitTransport *transport,
gconstpointer data)
{
const TestFixture *fixture = data;
const gchar *user;
const gchar *password;
password = fixture->client_password ? fixture->client_password : PASSWORD;
user = fixture->username ? fixture->username : g_get_user_name ();
do_basic_auth (transport, "*", user, password);
}
static JsonObject *
wait_until_transport_init (CockpitTransport *transport,
const gchar *expect_problem)
{
JsonObject *init = NULL;
guint sig;
const gchar *problem;
sig = g_signal_connect (transport, "control",
G_CALLBACK (on_control_get_options),
&init);
WAIT_UNTIL (init != NULL);
g_signal_handler_disconnect (transport, sig);
g_assert_cmpstr (json_object_get_string_member (init, "command"),
==, "init");
g_assert (cockpit_json_get_string (init, "problem", NULL, &problem));
g_assert_cmpstr (problem, ==, expect_problem);
return init;
}
static void
do_echo_and_close (TestCase *tc)
{
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);
g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
cockpit_transport_send (tc->transport, "546", sent);
while (received == NULL && !closed)
g_main_context_iteration (NULL, TRUE);
g_assert (!closed);
g_assert (g_bytes_equal (received, sent));
g_bytes_unref (sent);
g_bytes_unref (received);
received = NULL;
cockpit_transport_close (tc->transport, NULL);
while (received == NULL && !closed)
g_main_context_iteration (NULL, TRUE);
g_assert (closed);
g_assert (received == NULL);
}
static void
test_echo_and_close (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
do_fixture_auth (tc->transport, data);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
}
static void
test_echo_queue (TestCase *tc,
gconstpointer data)
{
GBytes *sent;
gint state = 0;
gboolean closed = FALSE;
JsonObject *init = NULL;
do_fixture_auth (tc->transport, data);
init = wait_until_transport_init (tc->transport, NULL);
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);
while (state != 2)
g_main_context_iteration (NULL, TRUE);
/* Only closes after above are sent */
cockpit_transport_close (tc->transport, NULL);
while (!closed)
g_main_context_iteration (NULL, TRUE);
json_object_unref (init);
}
static void
test_echo_large (TestCase *tc,
gconstpointer data)
{
GBytes *received = NULL;
GBytes *sent;
JsonObject *init = NULL;
do_fixture_auth (tc->transport, data);
init = wait_until_transport_init (tc->transport, NULL);
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);
while (received == NULL)
g_main_context_iteration (NULL, TRUE);
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);
while (received == NULL)
g_main_context_iteration (NULL, TRUE);
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);
while (received == NULL)
g_main_context_iteration (NULL, TRUE);
g_assert (g_bytes_equal (received, sent));
g_bytes_unref (sent);
g_bytes_unref (received);
received = NULL;
cockpit_transport_close (tc->transport, NULL);
json_object_unref (init);
}
static const gchar MOCK_RSA_KEY[] = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCYzo07OA0H6f7orVun9nIVjGYrkf8AuPDScqWGzlKpAqSipoQ9oY/mwONwIOu4uhKh7FTQCq5p+NaOJ6+Q4z++xBzSOLFseKX+zyLxgNG28jnF06WSmrMsSfvPdNuZKt9rZcQFKn9fRNa8oixa+RsqEEVEvTYhGtRf7w2wsV49xIoIza/bln1ABX1YLaCByZow+dK3ZlHn/UU0r4ewpAIZhve4vCvAsMe5+6KJH8ft/OKXXQY06h6jCythLV4h18gY/sYosOa+/4XgpmBiE7fDeFRKVjP3mvkxMpxce+ckOFae2+aJu51h513S9kxY2PmKaV/JU9HBYO+yO4j+j24v";
static const gchar MOCK_RSA_FP[] = "0e:6a:c8:b1:07:72:e2:04:95:9f:0e:b3:56:af:48:e2";
static void
do_auth_conversation (CockpitTransport *transport,
const gchar *expect_prompt,
const gchar *expect_json,
const gchar *response,
gboolean add_header)
{
JsonObject *auth = NULL;
GBytes *payload = NULL;
const gchar *cookie;
const gchar *challenge = NULL;
guint sig = 0;
gchar *encoded = NULL;
gchar *full = NULL;
gchar *result = NULL;
if (add_header)
{
encoded = g_base64_encode ((guchar *)response, strlen (response));
full = g_strdup_printf ("x-conversation id %s", encoded);
}
else
{
full = g_strdup (response);
}
sig = g_signal_connect (transport, "control",
G_CALLBACK (on_control_get_options),
&auth);
WAIT_UNTIL (auth != NULL);
g_signal_handler_disconnect (transport, sig);
g_assert (cockpit_json_get_string (auth, "cookie", NULL, &cookie));
g_assert_cmpstr (json_object_get_string_member (auth, "command"),
==, "authorize");
g_assert_cmpstr (cookie, !=, NULL);
challenge = json_object_get_string_member (auth, "challenge");
result = cockpit_authorize_parse_x_conversation (challenge, NULL);
g_assert_cmpstr (result, ==, expect_prompt);
json_object_remove_member (auth, "cookie");
json_object_remove_member (auth, "command");
json_object_remove_member (auth, "challenge");
cockpit_assert_json_eq (auth, expect_json);
payload = cockpit_transport_build_control ("command", "authorize",
"cookie", "cookie",
"response", full,
NULL);
cockpit_transport_send (transport, NULL, payload);
g_bytes_unref (payload);
g_free (full);
g_free (encoded);
g_free (result);
json_object_unref (auth);
}
static void
do_hostkey_conversation (TestCase *tc,
const gchar *response,
gboolean add_header)
{
gchar *expect_json = NULL;
expect_json = g_strdup_printf ("{\"message\": \"The authenticity of host '127.0.0.1:%d' can't be established. Do you want to proceed this time?\", \"default\": \"%s\", \"host-key\": \"[127.0.0.1]:%d %s\", \"echo\": true }",
(int)tc->ssh_port, MOCK_RSA_FP,
(int)tc->ssh_port, MOCK_RSA_KEY);
do_auth_conversation (tc->transport, "MD5 Fingerprint (ssh-rsa):",
expect_json, response, add_header);
g_free (expect_json);
}
static void
check_host_key_values (TestCase *tc,
JsonObject *init)
{
gchar *knownhosts = g_strdup_printf ("[127.0.0.1]:%d %s",
(int)tc->ssh_port,
MOCK_RSA_KEY);
g_assert_cmpstr (json_object_get_string_member (init, "host-key"),
==, knownhosts);
g_assert_cmpstr (json_object_get_string_member (init, "host-fingerprint"),
==, MOCK_RSA_FP);
g_assert (json_object_has_member (init, "invalid-hostkey-file") == FALSE);
g_free (knownhosts);
}
static void
test_problem (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
const TestFixture *fix = data;
do_fixture_auth (tc->transport, data);
init = wait_until_transport_init (tc->transport, fix->problem);
json_object_unref (init);
}
static const TestFixture fixture_unknown_host = {
.knownhosts_file = "/dev/null",
.problem = "unknown-host"
};
static const TestFixture fixture_ignore_hostkey = {
.knownhosts_file = "/dev/null",
.knownhosts_data = "*"
};
static const TestFixture fixture_hostkey_config = {
.knownhosts_file = "/dev/null",
.config = SRCDIR "/src/ws/mock-config"
};
static const TestFixture fixture_knownhost_data = {
.knownhosts_data = MOCK_RSA_KEY,
.knownhosts_file = "/dev/null",
.ssh_command = BUILDDIR "/mock-echo"
};
static void
test_knownhost_data (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
/* This test should validate in spite of not having known_hosts */
g_assert (fix->knownhosts_data != NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
test_echo_and_close (tc, data);
}
static void
test_bad_knownhost_data (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
/*
* This tail should fail in spite of having key in known_hosts,
* because expect_key is set.
*/
g_assert (fix->knownhosts_data != NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, NULL);
test_problem (tc, data);
}
static const TestFixture fixture_authorize_host_key = {
.knownhosts_data = "authorize",
.knownhosts_file = "/dev/null",
.allow_unknown = TRUE,
.ssh_command = BUILDDIR "/mock-echo"
};
static const TestFixture fixture_host_key_invalid = {
.knownhosts_data = NULL,
.knownhosts_file = SRCDIR "/src/ssh/invalid_known_hosts",
};
static const TestFixture fixture_prompt_host_key = {
.knownhosts_file = "/dev/null",
.allow_unknown = TRUE,
.ssh_command = BUILDDIR "/mock-echo"
};
static void
test_invalid_knownhost (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, SRCDIR "/src/ssh/invalid_known_hosts");
do_auth_response (tc->transport, "*", "");
init = wait_until_transport_init (tc->transport, "invalid-hostkey");
g_assert_cmpstr (json_object_get_string_member (init, "invalid-hostkey-file"),
==, fix->knownhosts_file);
json_object_unref (init);
}
static void
test_knownhost_data_prompt_blank (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, "authorize");
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_auth_response (tc->transport, "*", "");
do_auth_response (tc->transport, "x-host-key", "");
do_hostkey_conversation (tc, "", FALSE);
init = wait_until_transport_init (tc->transport, "unknown-hostkey");
check_host_key_values (tc, init);
json_object_unref (init);
}
static void
test_knownhost_data_prompt (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
gchar *knownhosts = g_strdup_printf ("x-host-key [127.0.0.1]:%d %s",
(int)tc->ssh_port,
MOCK_RSA_KEY);
g_assert_cmpstr (fix->knownhosts_data, ==, "authorize");
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_fixture_auth (tc->transport, data);
do_auth_response (tc->transport, "x-host-key", knownhosts);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
g_free (knownhosts);
}
static void
test_knownhost_data_prompt_bad (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, "authorize");
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_auth_response (tc->transport, "*", "");
do_auth_response (tc->transport, "x-host-key", "x-host-key");
init = wait_until_transport_init (tc->transport, "invalid-hostkey");
check_host_key_values (tc, init);
json_object_unref (init);
}
static void
test_hostkey_unknown (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_auth_response (tc->transport, "*", "");
do_hostkey_conversation (tc, "", FALSE);
init = wait_until_transport_init (tc->transport, "unknown-hostkey");
check_host_key_values (tc, init);
json_object_unref (init);
}
static void
test_hostkey_conversation (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_fixture_auth (tc->transport, data);
do_hostkey_conversation (tc, MOCK_RSA_FP, TRUE);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
}
static void
test_hostkey_conversation_bad (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_auth_response (tc->transport, "*", "");
do_hostkey_conversation (tc, "other-value", TRUE);
init = wait_until_transport_init (tc->transport, "unknown-hostkey");
check_host_key_values (tc, init);
json_object_unref (init);
}
static void
test_hostkey_conversation_invalid (TestCase *tc,
gconstpointer data)
{
const TestFixture *fix = data;
JsonObject *init = NULL;
g_assert_cmpstr (fix->knownhosts_data, ==, NULL);
g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
do_auth_response (tc->transport, "*", "");
do_hostkey_conversation (tc, "other-value", FALSE);
init = wait_until_transport_init (tc->transport, "unknown-hostkey");
check_host_key_values (tc, init);
json_object_unref (init);
}
static const TestFixture fixture_wrong_knownhost_data = {
.knownhosts_data = "wrong key",
.problem = "invalid-hostkey"
};
static const TestFixture fixture_invalid_knownhost_data = {
.knownhosts_data = "* invalid key",
.problem = "invalid-hostkey"
};
/* The output from this will go to stderr */
static const TestFixture fixture_bad_command = {
.ssh_command = "/nonexistant",
.problem = "no-cockpit"
};
/* Yes this makes a difference with bash, output goes to stdout */
static const TestFixture fixture_command_not_found = {
.ssh_command = "nonexistant-command",
.problem = "no-cockpit"
};
/* A valid command that exits with 0 */
static const TestFixture fixture_command_exits = {
.ssh_command = "/usr/bin/true",
.problem = "no-cockpit"
};
/* A valid command that exits with 1 */
static const TestFixture fixture_command_fails = {
.ssh_command = "/usr/bin/false",
.problem = "no-cockpit"
};
/* An ssh command that just kills itself with SIGTERM */
static const TestFixture fixture_terminate_problem = {
.ssh_command = "kill $$",
.problem = "terminated"
};
static const TestFixture fixture_unsupported_auth = {
.mock_sshd_arg = "--broken-auth",
};
static void
test_unsupported_auth (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_fixture_auth (tc->transport, data);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"no-server-support\",\"public-key\":\"no-server-support\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static const TestFixture fixture_auth_failed = {
.client_password = "bad password",
};
static void
test_auth_failed (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_fixture_auth (tc->transport, data);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_cannot_connect (void)
{
const gchar *argv[] = {
BUILDDIR "/cockpit-ssh",
"localhost:65533",
NULL,
};
JsonObject *init = NULL;
gchar **env = setup_env (NULL);
CockpitTransport *transport = start_bridge (env, (gchar **) argv);
do_basic_auth (transport, "*", "user", "unused");
init = wait_until_transport_init (transport, "no-host");
g_object_unref (transport);
json_object_unref (init);
g_strfreev (env);
}
/* test_rsa */
static const gchar MOCK_PRIV_KEY[] = "-----BEGIN RSA PRIVATE KEY-----\n"
"MIIEowIBAAKCAQEAvkPEj9GX9I0v/3dxCUB73TgOYjxkXB/m2ecKnUYmYtEwgirA\n"
"onCgZRMAvB7UaP5e6U/pNCXuZ+UgS0yU6tqEXD7MQ4YZiiNU1RaLe/gQ21NEx27h\n"
"hCGTZOLKcSfOFv2Z77OUcXSop2PZxQweYaH1+RB7hojOd7ZchN/tIBxvea5JSg/0\n"
"wLC8Lm65gpCZCxG2TNgfymovnyrYB44HnwEm4jCMU4uP68h0+D297US4oWwcpcqE\n"
"2S4LOxazjw1Brvntpqwtq624tUb1QVYMxdHpCR7Qu843r3XSpS4BwrnOks7Sbgyg\n"
"tHiKgogY5Xhu7ZqsTODtzyJ950YD0scnY41qHQIDAQABAoIBAFlQHnkUfixCCoH1\n"
"Y45gQsS5h6b9im7kWs128ziYsXQ5lnfD8eFO1TwdC39DSZpvrcX/yQy9sYf7uoke\n"
"Tdlg8jkLEX+w91Qs+al9h8SN0fvivqqPljUcPcBh5X3wnYGVUil/NvN7O6A38wXY\n"
"hnp2OKzN2+5vUdxIMm39X6ZvMrT/FyQjvdp393G4f0blYl7Npdc+HYPNnhHdgi4I\n"
"NUa32pG3ypoWkQRAYApaG2RXPTWQXTM2w4CFK5uJx/pB3r5NidU/H0XAl4TAuw9M\n"
"V9hrIPAOh5zKvHcPv8xOwR0Bt36F+/QATjO9pvlzQO6Rn3x2dyAVdaFMgdYTNpQQ\n"
"t0ZYsYECgYEA8yAhKUnArEQ4A+AI+pCtZuftzkXmnQ5SHNUtF2GeR5tRZ1PBF/tp\n"
"zoVRW+5ge1hI2VEx3ziGHEIBr7FfVej7twQ3URv5ILYj6CoNOf+HxkZgkTDGpYdj\n"
"AVvyjeD5qJEwCSeJ2bxD5LmxS9is8b8rXjVKRuPxwLeWqEjemPb0KNUCgYEAyFcL\n"
"TdN9cZghuzLZ0vfP4k9Hratunskz5njTFKnJx90riE7VqPH9OHvTeHn1xJ5WACnb\n"
"mFpAUG1v7BmC+WLEIPnKRKvuzL5C1yr+mntwTZsrwsLDdT/nfTS9hWzk9U6ykhJA\n"
"De8nNfxHuCoqM++CNvh+rA4W2Zc6WmE0uCwXYCkCgYEA70KMP+Sb3yvXcEDWtTcR\n"
"3raZ+agitib0ufk0QdFIgbGhH6111lMOIjZjBbSGcHxGXM8h5Ens+PwgSrWkW5hH\n"
"tylIAuMjfYShu4U+tPf6ty5lNB0rMJUW4qyI/AUNzEztV+T4LTWwHvR7PWgDcniu\n"
"hiytZyxFqmFBu2TS4vgM+e0CgYAvAL0WNVhpHlhLo1KXvKx5XEBk7qO1fV8/43ki\n"
"j/NXgPyFrnlSefP/HI4w5exThRKIV0m+JO6R8BsiOZoRCKsbUX+zPON6BemIsf2q\n"
"IOvoSU+rEibpi2S0a3tLopDVPPGIc9+zZTi94cKx4rKkHL1gSEzv8R5LTr/SFJxZ\n"
"2X5igQKBgBTkIeB0yI2PGf1drI+YbhDfgIphEeSCPbWxcUzPCcwLqXGo41cr8RXY\n"
"TgWtKk0gXhJWkMSIIXrfucCvXHTkk8wlqqgAVwrTgq4Q16LfBuucLwSe4TLp4SJZ\n"
"Lko5CzOq+EIv6DIlZ3tRHeDFatWe+41w27KhrV9yxB6Ay0MalP4i\n"
"-----END RSA PRIVATE KEY-----";
/* mock_dsa_key */
static const gchar MOCK_DSA_KEY[] = "-----BEGIN DSA PRIVATE KEY-----\n"
"MIIBugIBAAKBgQCCt0UxFgcPqwD3GFDNkKuJBMOfYF6VEP1r5HXmO0AzuuDB2mqK\n"
"8ko/MbK2jbnZkBYeMW/4uUNRDJzXIThcbYpX1OW1CYHU73rcmRFhS/th8agbPBml\n"
"kcgdb7UhQMNxjvFVBJ4xfOODd3Tci6HNDV/CL88DSGkIaOik7LnkJRtV/QIVAJdS\n"
"XhrlS8SUvi2GL/xCXFHk+0R7AoGAajaZeTEwcSkLuY09PlgEmu6QKsE+d6H7+2Uw\n"
"yBKJGEW+e/58Mw4JHLNX7AUayOnnMyf1ZV1sCm7IJMdjYd2YlmMAvh2ObqkaQ2o9\n"
"xxEQuizJ+Hc3XJdvX2Hs4hImwm0YyV+ZWRdryGgNRML/Mk9FJbp8h2UYssOFpRIJ\n"
"ZH/zSEwCgYBxLsdBBXn+8qEYwWK9KT+arRqNXC/lrl0Fp5YyxGNGCv82JcnuOShG\n"
"GTzhYf8AtTCY1u5oixiW9kea6KXGAKgTjfJShr7n47SZVfOPOrBT3VLhRdGGO3Gb\n"
"lDUppzfL8wsEdoqXjzrJuxSdrGnkFu8S9QjkPn9dCtScvWEcluHqMwIUUd82Co5T\n"
"738f4g+9Iuj9G/rNdfg=\n"
"-----END DSA PRIVATE KEY-----";
static void
test_key_good (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
gchar *msg = g_strdup_printf ("private-key %s", MOCK_PRIV_KEY);
do_auth_response (tc->transport, "*", msg);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
g_free (msg);
}
static void
test_key_fail (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
gchar *msg = g_strdup_printf ("private-key %s", MOCK_DSA_KEY);
do_auth_response (tc->transport, "*", msg);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"not-provided\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
g_free (msg);
}
static void
test_key_invalid (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_auth_response (tc->transport, "*", "private-key invalid");
init = wait_until_transport_init (tc->transport, "internal-error");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"not-provided\",\"public-key\":\"error\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_password_good (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
gchar *msg = g_strdup_printf ("password %s", PASSWORD);
do_auth_response (tc->transport, "*", msg);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
g_free (msg);
}
static void
test_password_fail (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_auth_response (tc->transport, "*", "password bad");
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_basic_no_user (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_basic_auth (tc->transport, "*", "", PASSWORD);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{}");
json_object_unref (init);
}
static void
test_basic_user_mismatch (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
/* Auth fails because user doesn't match */
do_basic_auth (tc->transport, "*", "other", PASSWORD);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_basic_secondary_no_user (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
do_auth_response (tc->transport, "*", "");
/* Auth succeeds because user is already set */
do_basic_auth (tc->transport, "basic", "", PASSWORD);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
}
static void
test_basic_secondary_user_mismatch (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
do_auth_response (tc->transport, "*", "");
/* Auth succeeds because secondary user is ignored */
do_basic_auth (tc->transport, "basic", "bad-user", PASSWORD);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
}
static const TestFixture fixture_multi_auth = {
.mock_sshd_arg = "--multi-step",
};
static void
test_multi_auth (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
do_fixture_auth (tc->transport, data);
do_auth_conversation (tc->transport, "Token",
"{\"message\":\"Password and Token\",\"echo\":true}",
"5", TRUE);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
}
static void
test_multi_auth_fail (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_fixture_auth (tc->transport, data);
do_auth_conversation (tc->transport, "Token",
"{\"message\":\"Password and Token\",\"echo\":true}",
"4", TRUE);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_multi_auth_empty (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_fixture_auth (tc->transport, data);
do_auth_conversation (tc->transport, "Token",
"{\"message\":\"Password and Token\",\"echo\":true}",
"", FALSE);
init = wait_until_transport_init (tc->transport, "internal-error");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"error\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_multi_auth_bad (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_fixture_auth (tc->transport, data);
do_auth_conversation (tc->transport, "Token",
"{\"message\":\"Password and Token\",\"echo\":true}",
"invalid", FALSE);
init = wait_until_transport_init (tc->transport, "internal-error");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"error\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
static void
test_multi_auth_3 (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
do_fixture_auth (tc->transport, data);
do_auth_conversation (tc->transport, "Token",
"{\"message\":\"Password and Token\",\"echo\":true}",
"6", TRUE);
do_auth_conversation (tc->transport, "So Close",
"{\"message\":\"Again\",\"echo\":false}",
"5", TRUE);
init = wait_until_transport_init (tc->transport, NULL);
do_echo_and_close (tc);
json_object_unref (init);
}
static void
test_multi_auth_3_fail (TestCase *tc,
gconstpointer data)
{
JsonObject *init = NULL;
JsonObject *auth_results = NULL;
do_fixture_auth (tc->transport, data);
do_auth_conversation (tc->transport, "Token",
"{\"message\":\"Password and Token\",\"echo\":true}",
"6", TRUE);
do_auth_conversation (tc->transport, "So Close",
"{\"message\":\"Again\",\"echo\":false}",
"4", TRUE);
init = wait_until_transport_init (tc->transport, "authentication-failed");
auth_results = json_object_get_object_member (init, "auth-method-results");
cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
json_object_unref (init);
}
int
main (int argc,
char *argv[])
{
cockpit_test_init (&argc, &argv);
g_test_add ("/ssh-bridge/echo-message", TestCase, &fixture_mock_echo,
setup, test_echo_and_close, teardown);
g_test_add ("/ssh-bridge/echo-queue", TestCase, &fixture_mock_echo,
setup, test_echo_queue, teardown);
g_test_add ("/ssh-bridge/echo-large", TestCase, &fixture_cat,
setup, test_echo_large, teardown);
g_test_add ("/ssh-bridge/bad-command", TestCase, &fixture_bad_command,
setup, test_problem, teardown);
g_test_add ("/ssh-bridge/command-not-found", TestCase, &fixture_command_not_found,
setup, test_problem, teardown);
g_test_add ("/ssh-bridge/command-not-cockpit", TestCase, &fixture_command_exits,
setup, test_problem, teardown);
g_test_add ("/ssh-bridge/command-just-fails", TestCase, &fixture_command_fails,
setup, test_problem, teardown);
g_test_add_func ("/ssh-bridge/cannot-connect", test_cannot_connect);
g_test_add ("/ssh-bridge/terminate-problem", TestCase, &fixture_terminate_problem,
setup, test_problem, teardown);
g_test_add ("/ssh-bridge/unsupported-auth", TestCase, &fixture_unsupported_auth,
setup, test_unsupported_auth, teardown);
g_test_add ("/ssh-bridge/auth-failed", TestCase,
&fixture_auth_failed, setup,
test_auth_failed, teardown);
g_test_add ("/ssh-bridge/key-good", TestCase, &fixture_mock_echo,
setup, test_key_good, teardown);
g_test_add ("/ssh-bridge/key-invalid", TestCase, &fixture_mock_echo,
setup, test_key_invalid, teardown);
g_test_add ("/ssh-bridge/key-fail", TestCase, &fixture_mock_echo,
setup, test_key_fail, teardown);
g_test_add ("/ssh-bridge/password-fail", TestCase, &fixture_mock_echo,
setup, test_password_fail, teardown);
g_test_add ("/ssh-bridge/password-good", TestCase, &fixture_mock_echo,
setup, test_password_good, teardown);
g_test_add ("/ssh-bridge/basic-no-user", TestCase, &fixture_mock_echo,
setup, test_basic_no_user, teardown);
g_test_add ("/ssh-bridge/basic-secondary-no-user", TestCase, &fixture_mock_echo,
setup, test_basic_secondary_no_user, teardown);
g_test_add ("/ssh-bridge/basic-user-mismatch", TestCase, &fixture_mock_echo,
setup, test_basic_user_mismatch, teardown);
g_test_add ("/ssh-bridge/basic-secondary-user-mismatch", TestCase, &fixture_mock_echo,
setup, test_basic_secondary_user_mismatch, teardown);
g_test_add ("/ssh-bridge/kb-multi-bad", TestCase,
&fixture_multi_auth,
setup, test_multi_auth_bad, teardown);
g_test_add ("/ssh-bridge/kb-multi-empty", TestCase,
&fixture_multi_auth,
setup, test_multi_auth_empty, teardown);
g_test_add ("/ssh-bridge/kb-multi-fail", TestCase,
&fixture_multi_auth,
setup, test_multi_auth_fail, teardown);
g_test_add ("/ssh-bridge/kb-multi-echo-message", TestCase,
&fixture_multi_auth,
setup, test_multi_auth, teardown);
g_test_add ("/ssh-bridge/kb-multi-3-fail", TestCase,
&fixture_multi_auth,
setup, test_multi_auth_3_fail, teardown);
g_test_add ("/ssh-bridge/kb-multi-3-echo-message", TestCase,
&fixture_multi_auth,
setup, test_multi_auth_3, teardown);
g_test_add ("/ssh-bridge/ignore-hostkey", TestCase, &fixture_ignore_hostkey,
setup, test_echo_and_close, teardown);
g_test_add ("/ssh-bridge/ignore-hostkey-configured", TestCase, &fixture_hostkey_config,
setup, test_echo_and_close, teardown);
g_test_add ("/ssh-bridge/unknown-host", TestCase, &fixture_unknown_host,
setup, test_problem, teardown);
g_test_add ("/ssh-bridge/knownhost-data", TestCase, &fixture_knownhost_data,
setup, test_knownhost_data, teardown);
g_test_add ("/ssh-bridge/wrong-knownhost-data", TestCase, &fixture_wrong_knownhost_data,
setup, test_bad_knownhost_data, teardown);
g_test_add ("/ssh-bridge/invalid-knownhost-data", TestCase, &fixture_invalid_knownhost_data,
setup, test_bad_knownhost_data, teardown);
g_test_add ("/ssh-bridge/knownhost-authorize", TestCase, &fixture_authorize_host_key,
setup, test_knownhost_data_prompt, teardown);
g_test_add ("/ssh-bridge/knownhost-authorize-blank", TestCase, &fixture_authorize_host_key,
setup, test_knownhost_data_prompt_blank, teardown);
g_test_add ("/ssh-bridge/knownhost-invalid", TestCase, &fixture_host_key_invalid,
setup, test_invalid_knownhost, teardown);
g_test_add ("/ssh-bridge/knownhost-authorize-bad", TestCase, &fixture_authorize_host_key,
setup, test_knownhost_data_prompt_bad, teardown);
g_test_add ("/ssh-bridge/hostkey-unknown", TestCase, &fixture_prompt_host_key,
setup, test_hostkey_unknown, teardown);
g_test_add ("/ssh-bridge/hostkey-conversation", TestCase, &fixture_prompt_host_key,
setup, test_hostkey_conversation, teardown);
g_test_add ("/ssh-bridge/hostkey-conversation-bad", TestCase, &fixture_prompt_host_key,
setup, test_hostkey_conversation_bad, teardown);
g_test_add ("/ssh-bridge/hostkey-conversation-invalid", TestCase, &fixture_prompt_host_key,
setup, test_hostkey_conversation_invalid, teardown);
return g_test_run ();
}